Metrics tell you that something's wrong, logs tell you why. If you have to SSH into the server and dig through docker logs across ten containers every time something breaks, you're losing valuable time. In this tutorial you'll extend the monitoring stack from Part 1 with centralized logging using Grafana Loki: all container logs end up searchable in one place, right next to your metrics in Grafana.
25 minIntermediateTested on Ubuntu 24.04, DockerUpdated 2026-07-07
In short
Two containers are added to the existing stack: Loki stores logs, and Grafana Alloy automatically collects them from all Docker containers. In Grafana, you can then search all your logs with LogQL — including filters by container, time range, and error message.
The classic ELK stack (Elasticsearch, Logstash, Kibana) is powerful, but overkill for a single VPS: Elasticsearch alone wants several GB of RAM. Loki takes a different approach — instead of indexing the entire log content, it only indexes labels like container name and host. This makes it dramatically leaner: the entire logging stack here runs on around 200–400 MB RAM. And since Grafana is already running, you don't need to learn a new interface, logs and metrics live in the same dashboard.
The division of labor is the same as with Prometheus: Loki is the database, Grafana Alloy is the collector that gathers logs and ships them to Loki, and Grafana is the interface for searching and visualizing.
Promtail is deprecated
Older tutorials use Promtail as the log collector. Promtail has been officially deprecated by Grafana (end of life: March 2026) and is being replaced by Grafana Alloy. For new setups, you should use Alloy directly — which is exactly what we do here.
Switch to your monitoring directory from Part 1 and create the configuration:
Create loki/loki-config.yml:
Two settings matter here: retention_period: 30d automatically deletes logs after 30 days (the compactor handles this), and the local filesystem is used as storage, completely sufficient for a single VPS, with no S3 bucket or external dependencies required.
Alloy automatically discovers all running containers via the Docker socket and reads their logs without you needing to change anything on your existing containers. Create alloy/config.alloy:
Adjust the host label to your server's name. Once you're later shipping logs from multiple servers into the same Loki instance, this is how you tell them apart.
Alloy reads logs from the JSON files Docker writes for each container — and those grow without limit until the disk fills up. Since logs now live centrally in Loki anyway, you can keep the local Docker logs small. Create or extend /etc/docker/daemon.json:
Then run sudo systemctl restart docker. Note: restarting the Docker daemon restarts all containers — plan accordingly. This setting only applies to newly created containers; existing ones pick it up after a docker compose up -d --force-recreate.
Connections → Data sources → Add data source → Loki
Enter http://loki:3100 as the URL (container name, just like with Prometheus)
Save & test
Then switch to Explore, select Loki as the data source, and try your first query. LogQL works similarly to PromQL — filter by labels first, then search the text:
shows all logs from the Grafana container. It gets more interesting with filters:
searches the logs of all containers on your server for "error". And with a rate function, you can turn logs back into metrics:
counts error messages per container over the last five minutes — perfect as a panel next to your CPU and RAM graphs from Part 1.
The real payoff shows up when the two work together: open your Node Exporter dashboard from Part 1, add a new panel, select Loki as the data source, and choose Logs as the visualization. With the query {host="my-vps"} |= "error", you can now see directly underneath the CPU graph which errors occurred at the moment of a load spike. This is exactly why you build centralized logging: cause and effect at a glance, no SSH session required.
Alerts on logs are also possible, under Alerting in Grafana, you can create a rule based on a LogQL query, for example when a container's error rate crosses a threshold. The notification then goes through the same Alertmanager and contact points as your metric alerts from Part 1.
So far we've only collected container logs. If you also want SSH logins, kernel messages, and other system logs centralized, add this to alloy/config.alloy:
And add two additional volumes to the Alloy service in the compose file:
After docker compose up -d --force-recreate alloy, you'll find the system logs in Grafana under the label job="journal" — handy for, say, keeping an eye on failed SSH login attempts.
Loki and Alloy together run at around 200–400 MB RAM on a typical VPS. Disk space needs depend on log volume: Loki compresses heavily, and a small server with a handful of containers usually stays well under 1 GB with 30 days of retention. If your log volume grows, reduce the retention_period first before upgrading the server.
Your server now has both: metrics that warn you, and logs that tell you why. All container logs (and optionally system logs) are centrally searchable, automatically rotated, and cleaned up after 30 days — with just two additional containers.
In the next part of our "Production-Ready Server" series, we'll compare Nginx, Caddy, and Traefik — which reverse proxy fits which setup, and when is it worth switching?
No. Alloy automatically discovers all containers via the Docker socket and reads their logs. No labels or logging-driver changes are needed on your existing containers.
Can I send logs from multiple servers into one Loki instance?
Yes. Just install Alloy on each additional server and point its loki.write block at the URL of your central Loki. Important: secure the connection via VPN, a private network, or a reverse proxy with authentication, since Loki has no built-in access protection with auth_enabled: false.
Why am I not seeing any logs in Grafana?
Check the chain from the start: Is Alloy running (docker compose ps)? Does Loki report status ready at /ready? The most common causes are an unmounted Docker socket in the Alloy container and a typo in the data source's Loki URL.
Promtail is deprecated (end of life March 2026) and is being replaced by Grafana Alloy. The concepts are identical, only the configuration language differs — new setups should build directly on Alloy.