Docker & DevOps

Centralise Docker logs with Loki and Promtail on a Linux VPS

Run Loki and Promtail in Docker Compose to collect container logs from every service on one server, then search and filter them in Grafana with LogQL.

AdvancedLinux server7 min read

Once you run more than a handful of containers, docker logs stops being enough: you have to remember container names, and old output disappears when a container is recreated. This guide centralises Docker logs with Loki and Promtail — Loki stores and indexes the log streams, Promtail tails every container on the host, and Grafana gives you one search box for all of them. You will end up with a Compose stack that labels each line with the container name and Compose service, so you can filter in seconds instead of guessing.

Before you start

You need:

  • A Linux VPS running Ubuntu 22.04/24.04, Debian 12, AlmaLinux 8–9 or Rocky 8–9 with Docker and the Compose plugin installed. If you have not done that yet, follow Install Docker and Docker Compose on a Linux VPS.
  • A user that can talk to the Docker socket, for example deploy in the docker group, or root.
  • Roughly 1 GB of free RAM for Loki, Promtail and Grafana together, plus disk space for the log store.
  • No firewall change needed for this setup: everything binds to 127.0.0.1 and stays on the server.

Note: Promtail mounts the Docker socket read-only, which still gives it broad visibility over the host's containers. Treat the Promtail container as privileged and keep port 9080 closed to the internet.

Create the project directory and the volume folders:

Bash
sudo mkdir -p /opt/logging/loki /opt/logging/promtail
cd /opt/logging

Write the Loki configuration

Loki needs a config file before it will start. Filesystem storage is the simplest option for a single VPS: no object storage account, no extra service.

Bash
nano /opt/logging/loki/loki-config.yaml

Paste this and save:

YAML
auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9096

common:
  instance_addr: 127.0.0.1
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

schema_config:
  configs:
    - from: 2024-04-01
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

limits_config:
  retention_period: 744h

compactor:
  working_directory: /loki/compactor
  retention_enabled: true
  delete_request_store: filesystem

The important parts:

  • auth_enabled: false is fine because Loki listens on localhost only. If you ever expose it, set up multi-tenancy and authentication first.
  • schema_config uses the TSDB index — the current default for new Loki installs. Keep the from date in the past; Loki rejects schema entries dated in the future.
  • retention_period and the compactor block delete old chunks. Adjust 744h to whatever fits your disk, then restart Loki.

Write the Promtail configuration

Promtail discovers containers through the Docker API, so you never list container names by hand. New containers are picked up automatically.

Bash
nano /opt/logging/promtail/promtail-config.yaml

Paste:

YAML
server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /var/lib/promtail/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: docker
    docker_sd_configs:
      - host: unix:///var/run/docker.sock
        refresh_interval: 5s
    relabel_configs:
      - source_labels: ['__meta_docker_container_name']
        regex: '/(.*)'
        target_label: container
      - source_labels: ['__meta_docker_container_log_stream']
        target_label: stream
      - source_labels: ['__meta_docker_container_label_com_docker_compose_service']
        target_label: service

positions.yaml records how far Promtail has read each log file, so it resumes after a restart instead of resending everything. The three relabel rules turn Docker metadata into Loki labels: container (name), stream (stdout or stderr) and service (the Compose service name, when the container has that label).

Tip: Add a fourth relabel rule for a custom label if you want to filter by environment, for example __meta_docker_container_label_environment → env, then set labels: {environment: production} on each service in your Compose files.

Note: Grafana Labs now steers new log-shipping setups towards Grafana Alloy. Promtail is still widely deployed and works fine, but check the vendor's documentation before you build long-term automation around it.

Centralise Docker logs with Loki and Promtail

Create the Compose file that runs all three services:

Bash
nano /opt/logging/docker-compose.yml
YAML
services:
  loki:
    image: grafana/loki:latest
    restart: unless-stopped
    command: -config.file=/etc/loki/loki-config.yaml
    volumes:
      - ./loki/loki-config.yaml:/etc/loki/loki-config.yaml:ro
      - loki-data:/loki
    ports:
      - "127.0.0.1:3100:3100"

  promtail:
    image: grafana/promtail:latest
    restart: unless-stopped
    command: -config.file=/etc/promtail/promtail-config.yaml
    depends_on:
      - loki
    volumes:
      - ./promtail/promtail-config.yaml:/etc/promtail/promtail-config.yaml:ro
      - promtail-positions:/var/lib/promtail
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
    ports:
      - "127.0.0.1:9080:9080"

  grafana:
    image: grafana/grafana:latest
    restart: unless-stopped
    environment:
      GF_SECURITY_ADMIN_USER: admin
      GF_SECURITY_ADMIN_PASSWORD: change-this-password
    volumes:
      - grafana-data:/var/lib/grafana
    ports:
      - "127.0.0.1:3000:3000"

volumes:
  loki-data:
  promtail-positions:
  grafana-data:

A few things worth knowing before you start it:

  • The image tags are latest for readability. Pin a specific version once you have confirmed it works, and read the release notes before upgrading — Loki schema and config keys do change between major versions.
  • depends_on only controls start order, not readiness. Promtail retries the push endpoint, so an early start is harmless. For stricter ordering, see Docker Compose depends_on and healthchecks.
  • The Grafana admin password sits in plain text in the file. Move it to a Compose secret or an .env file as described in Manage secrets and environment variables in Docker Compose.

Start the stack:

Bash
cd /opt/logging
sudo docker compose up -d
sudo docker compose ps

Expect three containers in state running. The first start downloads the images, so give it a minute.

Point Grafana at Loki and query logs

Promtail is already shipping logs, but nothing can read them yet.

  1. Open an SSH tunnel from your laptop so you never expose Grafana: ssh -L 3000:127.0.0.1:3000 deploy@203.0.113.10.
  2. Browse to http://localhost:3000 and sign in with the admin credentials from the Compose file.
  3. Go to Connections → Data sources → Add data source → Loki.
  4. Set the URL to http://loki:3100 — the Compose service name, not localhost.
  5. Click Save & test. You should see a confirmation that the data source is connected.

Then open Explore, pick the Loki data source and try these queries:

{container="web"}
{job="docker"} |= "error"
{service="api"} | json | status >= 500

The first lists every line from the container named web. The second filters all streams for the word error. The third parses JSON log lines and filters on a field, which works well for structured application logs. To see which containers are loudest, run:

sum by (container) (rate({job="docker"}[5m]))

Check it works

Verify each layer, from the shipper to the store:

Bash
curl -s http://127.0.0.1:3100/ready
curl -s http://127.0.0.1:3100/loki/api/v1/labels

The first should print ready. The second returns a JSON list of label names — you should see container, job, service and stream once at least one container has logged something.

Check that Promtail sees your containers:

Bash
curl -s http://127.0.0.1:9080/targets | head

Then generate a test line and confirm it arrives:

Bash
sudo docker run --rm --name logtest alpine:3.20 sh -c 'echo hello-from-logtest'

A few seconds later, {container="logtest"} in Grafana Explore shows hello-from-logtest. If it does, log shipping works end to end.

Troubleshooting

  • No log lines in Grafana, but the data source tests fine. The time picker is usually the culprit. Loki only returns what exists, so widen Explore to the last 24 hours and check the container is actually writing to stdout rather than to a file inside its own volume.
  • {service="..."} returns nothing. That container has no com.docker.compose.service label — it was started with docker run, not Compose. Query by container instead, or add the label yourself.
  • Logs arrive but stop after a restart. The promtail-positions volume is missing or was recreated. Confirm the volume is mounted at /var/lib/promtail and that positions.filename matches the path inside the container.
  • Loki exits with a schema or config error on start. A schema_config entry dated in the future, or a config key that moved between major versions. Read the error, then check the Loki configuration reference for your pinned tag.
  • Disk fills up and old logs are still there. Retention only deletes when the compactor can run, so confirm retention_enabled: true and that delete_request_store matches your storage backend. Compaction runs on an interval — do not expect instant deletion.

Warning: Deleting the loki-data volume removes every log line you have collected. If you need the history, export or copy the volume before you change the schema or run docker compose down -v.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.