Skip to content

LogWard LogWard

LogWard is an open-source, self-hosted log management tool.

🛠 Installation

Default Port: 3000

homelab/docker/logward

task up
docker compose up

âš™ Config

homelab/docker/logward/.env

# LogWard Docker Configuration
# =============================
# Copy this file to .env and set secure passwords
# =============================================================================
# REQUIRED - Set these before starting
# =============================================================================

# Database password (use a strong random password)
DB_PASSWORD=your_secure_database_password

# Redis password (use a strong random password)
REDIS_PASSWORD=your_secure_redis_password

# API key encryption secret (minimum 32 characters)
API_KEY_SECRET=your_32_character_secret_key_here

# =============================================================================
# OPTIONAL - Customize if needed
# =============================================================================

# Database settings (defaults work for most setups)
DB_NAME=logward
DB_USER=logward

# Frontend API URL (only change if backend is on different host)
# Default: http://localhost:8080
PUBLIC_API_URL=https://logward.l.nicholaswilde.io

# Docker images (pin versions for production stability)
# LOGWARD_BACKEND_IMAGE=logward/backend:0.3.1
# LOGWARD_FRONTEND_IMAGE=logward/frontend:0.3.1

# =============================================================================
# EMAIL NOTIFICATIONS (optional)
# =============================================================================
# SMTP_HOST=smtp.example.com
# SMTP_PORT=587
# [email protected]
# SMTP_PASS=your_smtp_password
# [email protected]

# =============================================================================
# HORIZONTAL SCALING (advanced)
# =============================================================================
# For scaling, use docker-compose.traefik.yml:
#   docker compose -f docker-compose.yml -f docker-compose.traefik.yml up -d --scale backend=3
#
# See docs: https://logward.dev/docs/deployment#horizontal-scaling
homelab/docker/logward/compose.yaml
---
# LogWard Docker Compose Configuration
# Basic usage: docker compose up -d
# With Docker log collection: docker compose --profile logging up -d

services:
  postgres:
    image: timescale/timescaledb:latest-pg16
    container_name: logward-postgres
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    command:
      - "postgres"
      - "-c"
      - "max_connections=100"
      - "-c"
      - "shared_buffers=256MB"
      - "-c"
      - "effective_cache_size=768MB"
      - "-c"
      - "work_mem=16MB"
      - "-c"
      - "maintenance_work_mem=128MB"
      # Parallel query settings for faster aggregations
      - "-c"
      - "max_parallel_workers_per_gather=4"
      - "-c"
      - "max_parallel_workers=8"
      - "-c"
      - "parallel_tuple_cost=0.01"
      - "-c"
      - "parallel_setup_cost=100"
      - "-c"
      - "min_parallel_table_scan_size=8MB"
      # Write-ahead log tuning for ingestion
      - "-c"
      - "wal_buffers=16MB"
      - "-c"
      - "checkpoint_completion_target=0.9"
      # Logging for slow queries (>100ms)
      - "-c"
      - "log_min_duration_statement=100"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
    networks:
      - logward-network

  redis:
    image: redis:7-alpine
    container_name: logward-redis
    command: redis-server --requirepass ${REDIS_PASSWORD}
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "sh", "-c", "redis-cli -a ${REDIS_PASSWORD} ping | grep -q PONG"]
      interval: 10s
      timeout: 3s
      retries: 5
    restart: unless-stopped
    networks:
      - logward-network

  backend:
    image: ghcr.io/logward-dev/logward-backend:0.3.2-rc1
    container_name: logward-backend
    ports:
      - "8080:8080"
    environment:
      NODE_ENV: production
      DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}
      DATABASE_HOST: postgres
      DB_USER: ${DB_USER}
      REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
      API_KEY_SECRET: ${API_KEY_SECRET}
      PORT: 8080
      HOST: 0.0.0.0
      SMTP_HOST: ${SMTP_HOST:-}
      SMTP_PORT: ${SMTP_PORT:-587}
      SMTP_USER: ${SMTP_USER:-}
      SMTP_PASS: ${SMTP_PASS:-}
      SMTP_FROM: ${SMTP_FROM:[email protected]}
      INTERNAL_LOGGING_ENABLED: ${INTERNAL_LOGGING_ENABLED:-false}
      INTERNAL_API_KEY: ${INTERNAL_API_KEY:-}
      SERVICE_NAME: logward-backend
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "node", "-e", "require('http').get('http://localhost:8080/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 40s
    restart: unless-stopped
    networks:
      - logward-network

  worker:
    image: ghcr.io/logward-dev/logward-backend:0.3.2-rc1
    container_name: logward-worker
    command: ["worker"]
    healthcheck:
      disable: true
    environment:
      NODE_ENV: production
      DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}
      DATABASE_HOST: postgres
      DB_USER: ${DB_USER}
      REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
      API_KEY_SECRET: ${API_KEY_SECRET}
      SMTP_HOST: ${SMTP_HOST:-}
      SMTP_PORT: ${SMTP_PORT:-587}
      SMTP_USER: ${SMTP_USER:-}
      SMTP_PASS: ${SMTP_PASS:-}
      SMTP_FROM: ${SMTP_FROM:[email protected]}
      INTERNAL_LOGGING_ENABLED: ${INTERNAL_LOGGING_ENABLED:-false}
      INTERNAL_API_KEY: ${INTERNAL_API_KEY:-}
      SERVICE_NAME: logward-worker
    depends_on:
      backend:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - logward-network

  frontend:
    image: ghcr.io/logward-dev/logward-frontend:0.3.2-rc1
    container_name: logward-frontend
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      PUBLIC_API_URL: ${PUBLIC_API_URL:-http://localhost:8080}
    depends_on:
      - backend
    restart: unless-stopped
    networks:
      - logward-network

  fluent-bit:
    # image: fluent/fluent-bit:latest
    image: fluent-bit-16k:latest
    container_name: logward-fluent-bit
    ports:
      - "514:514/udp"  # Syslog UDP
      - "514:514/tcp"  # Syslog TCP
    # profiles:
      # - logging
    volumes:
      - ./fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf:ro
      - ./parsers.conf:/fluent-bit/etc/parsers.conf:ro
      - ./extract_container_id.lua:/fluent-bit/etc/extract_container_id.lua:ro
      - ./wrap_logs.lua:/fluent-bit/etc/wrap_logs.lua:ro
      - ./map_syslog_level.lua:/fluent-bit/etc/map_syslog_level.lua:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      LOGWARD_API_KEY: ${FLUENT_BIT_API_KEY:-}
      LOGWARD_API_HOST: backend
    depends_on:
      - backend
    restart: unless-stopped
    networks:
      - logward-network

volumes:
  postgres_data:
    driver: local
  redis_data:
    driver: local

networks:
  logward-network:

📒 Logging

To enable automatic log collection from other Docker containers:

  1. Generate API Key: Log in to LogWard, go to Project Settings, and create an API Key.
  2. Update Config: Add the API key to your .env file.

    FLUENT_BIT_API_KEY=lp_your_api_key_here
    
  3. Create Config Files: Ensure the following configuration files exist in your docker/logward directory.

    fluent-bit.conf
    # Fluent Bit Configuration for LogWard
    
    [SERVICE]
        # Flush logs every 5 seconds
        Flush        5
        # Run in foreground
        Daemon       Off
        # Log level (error, warning, info, debug, trace)
        Log_Level    info
        # Parsers configuration file
        Parsers_File /fluent-bit/etc/parsers.conf
    
    # =============================================================================
    # INPUT - Docker Container Logs
    # =============================================================================
    [INPUT]
        Name              tail
        Path              /var/lib/docker/containers/*/*.log
        Parser            docker
        Tag               docker.*
        Refresh_Interval  5
        Mem_Buf_Limit     5MB
        Skip_Long_Lines   On
        Path_Key          filepath
    
    # =============================================================================
    # FILTER - Parse and Enrich
    # =============================================================================
    # Extract container metadata (name, id, image)
    [FILTER]
        Name                parser
        Match               docker.*
        Key_Name            log
        Parser              docker_json
        Reserve_Data        On
        Preserve_Key        On
    
    # Add required fields for LogWard API
    [FILTER]
        Name                modify
        Match               docker.*
        # Set default level if not present
        Add                 level info
        # Rename 'log' field to 'message'
        Rename              log message
        # Set service name from container_name
        Copy                container_name service
    
    # Remove unnecessary fields to reduce log size
    [FILTER]
        Name                record_modifier
        Match               docker.*
        Remove_key          stream
        Remove_key          filepath
        Remove_key          container_name
    
    # =============================================================================
    # OUTPUT - Send to LogWard
    # =============================================================================
    [OUTPUT]
        Name                http
        Match               docker.*
        Host                ${LOGWARD_API_HOST}
        Port                8080
        URI                 /api/v1/ingest/single
        Format              json_lines
        Header              X-API-Key ${LOGWARD_API_KEY}
        Header              Content-Type application/json
        # Date/time settings
        Json_date_key       time
        Json_date_format    iso8601
        # Retry settings
        Retry_Limit         3
        # TLS (disable for internal Docker network)
        tls                 Off
    
    parsers.conf
    # Fluent Bit Parsers Configuration
    
    # Parser for Docker JSON logs
    [PARSER]
        Name        docker_json
        Format      json
        Time_Key    time
        Time_Format %Y-%m-%dT%H:%M:%S.%L%z
        Time_Keep   On
    
    # Parser for Docker container logs
    [PARSER]
        Name        docker
        Format      json
        Time_Key    time
        Time_Format %Y-%m-%dT%H:%M:%S.%L%z
        Time_Keep   On
    
    extract_container_id.lua
    -- Extract container ID from Docker log file path
    -- Path format: /var/lib/docker/containers/<container_id>/<container_id>-json.log
    
    function extract_container_id(tag, timestamp, record)
        local filepath = record["filepath"]
    
        if filepath == nil then
            return 0, timestamp, record
        end
    
        -- Extract container ID from path
        -- Example: /var/lib/docker/containers/abc123.../abc123...-json.log
        local container_id = filepath:match("/var/lib/docker/containers/([^/]+)/")
    
        if container_id then
            record["container_id"] = container_id
            record["container_short_id"] = container_id:sub(1, 12)
        end
    
        -- Return code: 1 = modified and keep, 0 = no change
        return 1, timestamp, record
    end
    
    wrap_logs.lua
    -- Dummy file if not provided
    function wrap_logs(tag, timestamp, record)
        return 0, timestamp, record
    end
    
  4. Enable Logging: Start the stack with the logging profile.

    task up-logging
    
    docker compose --profile logging up -d
    

Integrations

CLI (curl)

You can send a single log entry to LogWard using curl.

curl -X POST https://logward.l.nicholaswilde.io/api/v1/ingest/single \
  -H "X-API-Key: lp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "service": "my-script",
    "level": "info",
    "message": "This is a test log message",
    "timestamp": "'"$(date -u +'%Y-%m-%dT%H:%M:%SZ')"'"
  }'

:material-proxmox: Proxmox (rsyslog)

To forward logs from a Proxmox node (or any Debian-based system) to LogWard via syslog:

  1. Install rsyslog (if not already installed):

    apt update && apt install rsyslog -y
    
  2. Create configuration file:

    /etc/rsyslog.d/50-logward.conf

    # Forward all logs to LogWard via TCP
    *.* @@YOUR_LOGWARD_IP:514
    

    Note: Use @@ for TCP (recommended) or @ for UDP.

  3. Restart rsyslog:

    systemctl restart rsyslog
    

Raspberry Pi

If you are running on a Raspberry Pi, the standard Fluent Bit docker image might not work because it requires a page size of 4k. The Raspberry Pi kernel often uses a page size of 16k.

To check your page size, run:

getconf PAGESIZE

If the output is 16384, you must generate a custom Fluent Bit docker image with jemalloc support disabled.

task build
docker build -t fluent-bit-16k:latest .

Ensure that your compose.yaml is using the custom image:

image: fluent-bit-16k:latest

Traefik

homelab/pve/traefik/conf.d/logward.yaml
---
http:
 #region routers 
  routers:
    logward:
      entryPoints:
        - "websecure"
      rule: "Host(`logward.l.nicholaswilde.io`)"
      middlewares:
        - default-headers@file
        - https-redirectscheme@file
      tls: {}
      service: logward
    logward-backend:
      entryPoints:
        - "websecure"
      rule: "Host(`logward.l.nicholaswilde.io`) && (PathPrefix(`/api`) || PathPrefix(`/health`))"
      middlewares:
        - default-headers@file
        - https-redirectscheme@file
      tls: {}
      service: logward-backend
#endregion
#region services
  services:
    logward:
      loadBalancer:
        servers:
          - url: "http://192.168.2.195:3000"
        passHostHeader: true
    logward-backend:
      loadBalancer:
        servers:
          - url: "http://192.168.2.195:8080"
        passHostHeader: true
        healthCheck:
          path: /health
          interval: 10s
#endregion
  middlewares:
    https-redirectscheme:
      redirectScheme:
        scheme: https
        permanent: true
    default-headers:
      headers:
        frameDeny: true
        browserXssFilter: true
        contentTypeNosniff: true
        forceSTSHeader: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 15552000
        customFrameOptionsValue: SAMEORIGIN
        customRequestHeaders:
          X-Forwarded-Proto: https

    default-whitelist:
      ipAllowList:
        sourceRange:
        - "10.0.0.0/8"
        - "192.168.0.0/16"
        - "172.16.0.0/12"

    secured:
      chain:
        middlewares:
        - default-whitelist
        - default-headers

🔧 Troubleshooting

If you are experiencing issues with log collection, follow these steps:

  1. Verify Fluent Bit is running:

    docker compose ps fluent-bit
    
  2. Check Fluent Bit logs:

    docker compose logs fluent-bit
    
  3. Test connectivity:

    Verify you can reach LogWard's Fluent Bit port (514) from your source device.

    nc -zv YOUR_LOGWARD_IP 514
    
  4. Verify API Key:

    Ensure FLUENT_BIT_API_KEY is correctly set in your .env file.

  5. Test Syslog Manually:

    Send a test message to verify the pipeline.

    echo "<14>Test syslog message from terminal" | nc -u -w1 YOUR_LOGWARD_IP 514
    
    echo "<14>Test syslog message from terminal" | nc -w1 YOUR_LOGWARD_IP 514
    

Task List


🔗 References