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
deployin thedockergroup, 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.1and 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:
sudo mkdir -p /opt/logging/loki /opt/logging/promtail
cd /opt/loggingWrite 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.
nano /opt/logging/loki/loki-config.yamlPaste this and save:
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: filesystemThe important parts:
auth_enabled: falseis fine because Loki listens on localhost only. If you ever expose it, set up multi-tenancy and authentication first.schema_configuses the TSDB index — the current default for new Loki installs. Keep thefromdate in the past; Loki rejects schema entries dated in the future.retention_periodand the compactor block delete old chunks. Adjust744hto 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.
nano /opt/logging/promtail/promtail-config.yamlPaste:
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: servicepositions.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:
nano /opt/logging/docker-compose.ymlservices:
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
latestfor 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_ononly 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
.envfile as described in Manage secrets and environment variables in Docker Compose.
Start the stack:
cd /opt/logging
sudo docker compose up -d
sudo docker compose psExpect 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.
- 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. - Browse to
http://localhost:3000and sign in with the admin credentials from the Compose file. - Go to Connections → Data sources → Add data source → Loki.
- Set the URL to
http://loki:3100— the Compose service name, notlocalhost. - 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 >= 500The 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:
curl -s http://127.0.0.1:3100/ready
curl -s http://127.0.0.1:3100/loki/api/v1/labelsThe 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:
curl -s http://127.0.0.1:9080/targets | headThen generate a test line and confirm it arrives:
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 nocom.docker.compose.servicelabel — it was started withdocker run, not Compose. Query bycontainerinstead, or add the label yourself.- Logs arrive but stop after a restart. The
promtail-positionsvolume is missing or was recreated. Confirm the volume is mounted at/var/lib/promtailand thatpositions.filenamematches the path inside the container. - Loki exits with a schema or config error on start. A
schema_configentry 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: trueand thatdelete_request_storematches 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
- Tune what Docker keeps locally so you are not storing the same lines twice: View and rotate Docker container logs.
- Add metrics next to your logs: Monitor Docker containers with Prometheus, cAdvisor and Grafana.
- Put Grafana behind HTTPS instead of an SSH tunnel: Traefik in Docker: automatic SSL and routing by container labels.
