2026-09-24

Observability that lives outside the app stack

When an application is unhealthy, the tools you use to look at it should not share its fate. So the observability stack for a production platform lives in its own compose file, deployed and restarted independently of the app.

Two stacks, one network

The application compose file runs the backend API, a migration container, an inventory worker, Redis and RabbitMQ. A second compose file runs everything for observability:

docker-compose.yaml                 backend, migrations, worker, Redis, RabbitMQ
docker-compose.observability.yaml   Grafana, Loki, Promtail, Prometheus, cAdvisor, node_exporter

They are joined by a single external Docker network. The app stack creates it, so the order matters: bring the app up first.

docker compose --env-file .env.production up -d --build

cp .env.observability.example .env.observability
# set at minimum GRAFANA_ADMIN_USER and GRAFANA_ADMIN_PASSWORD

docker compose -f docker-compose.observability.yaml --env-file .env.observability up -d

Who does what

Docker containers      -> Promtail  -> Loki        -> Grafana
Host + containers      -> node_exporter / cAdvisor -> Prometheus -> Grafana

Default to closed

Every observability service binds to 127.0.0.1 by default: Grafana on one port, Prometheus and Loki on others. Only Grafana is meant to be reverse-proxied through Nginx. The browser talks to Grafana, Grafana talks to Prometheus and Loki over the internal network, and neither of those two is on a public route unless someone deliberately exposes it later. It is the kind of default that costs nothing and prevents an embarrassing incident.

Being honest about coverage

The most useful section of the runbook is the list of things the stack does not do yet:

So the coverage today is host metrics, container metrics and centralised Docker logs. That is a deliberately narrow first slice, and writing the gaps down does two jobs: it stops anyone assuming a dashboard means the app is being watched, and it becomes the backlog.

One operational gotcha is documented too: cAdvisor depends on the host’s Docker storage layout. On a host that doesn’t use the default data directory you must set it explicitly, and on newer containerd snapshotter layouts cAdvisor can be noisier or less complete than node_exporter. A tool that is right about hosts but only mostly right about containers is worth knowing about before an incident, not during one.

Takeaways

← All posts