Containerize a Monitoring Stack
Containerize a monitoring stack with Docker in this hands-on Linux · networking · telemetry lesson — step-by-step, with troubleshooting and what to learn next.
Focus: containerize a monitoring stack with docker
Spinning up a monitoring stack by hand is a rite of passage that quickly turns into a nightmare: you install Prometheus, then Grafana, then Node Exporter, and suddenly you're fighting version mismatches, systemd units, and firewall rules on three different machines. Containerizing a monitoring stack with Docker solves that pain by packaging every component with its own dependencies, configuration, and network wiring into a single reproducible unit. In this lesson, you'll learn how to containerize a full observability stack with Docker — the same pattern used by teams running production telemetry at scale — and you'll get hands-on with a working Prometheus + Grafana example.
The Problem This Lesson Solves
Manual monitoring setups are fragile. Each tool — Prometheus for metrics, Grafana for dashboards, Node Exporter for host metrics — has its own runtime dependencies, config paths, and startup order. Upgrading one component can break another, and reproducing the same environment on a colleague's laptop is nearly impossible.
Docker fixes this by providing:
- Isolation: Each service runs in its own container, with its own filesystem and network namespace.
- Reproducibility: A
docker-compose.ymlfile is the single source of truth —docker compose upgives you the same stack everywhere. - Simplified networking: Docker's internal bridge network lets containers communicate by name, so you never hard-code IP addresses.
- Easy scaling: Add replicas, swap versions, or roll back with one command.
Pro tip: Think of
docker-compose.ymlas the blueprint of your monitoring stack — version it in Git, and your entire observability platform becomes auditable and shareable.
Core Concept / Mental Model
Imagine a monitoring stack as three layers:
- Data collection — Node Exporter scrapes host metrics (CPU, memory, disk).
- Data storage & querying — Prometheus pulls metrics from exporters, stores them in a time-series database, and exposes a query language (PromQL).
- Visualization — Grafana queries Prometheus and paints dashboards.
Without Docker, you'd install each tool system-wide, configure them to talk over TCP/IP, and pray the firewall doesn't get in the way. With Docker, each layer becomes a container, and containers talk to each other over a user-defined bridge network using service names as DNS aliases.
Here's the mental model in words:
- Containers are isolated processes — they share the host kernel but have their own filesystems and network stacks.
- Networking: Containers on the same bridge network can resolve each other by container name. Prometheus can just use
node-exporter:9100instead of192.168.1.10:9100. - Persistence: Container filesystems are ephemeral. Use Docker volumes to store Prometheus data and Grafana configs across restarts.
- Compose: Defines all containers, volumes, and networks in one YAML file — the single entry point for the entire stack.
Definitions You'll Use
- Image: A read-only template (e.g.,
prom/prometheus:v2.50.1). - Container: A running instance of an image.
- Bridge network: A private virtual network inside Docker.
- Bind mount: A directory on the host shared into the container (used for config files).
- Named volume: A Docker-managed volume for persistent data.
How It Works Step by Step
The containerization process follows a predictable sequence that you'll reuse for any stack:
- Choose images & versions — Pin exact versions (
prom/prometheus:v2.50.1,grafana/grafana:10.4.0,prom/node-exporter:v1.7.0). This makes the stack reproducible. - Write config files — Create a Prometheus config (
prometheus.yml) that lists scrape targets. In a containerized world, targets are service names, not IPs. - Define the compose file — Declare each service, its image, ports, volumes, and network membership.
- Set up volumes — Persist Prometheus data and Grafana dashboards/data.
- Expose the right ports — While internal traffic flows over the bridge, you expose only what's needed to the host:
9090(Prometheus UI),3000(Grafana),9100(optional if you scrape from the host). - Start and verify —
docker compose up -dand then check each endpoint.
The cause and effect here:
- Because containers use service names, you avoid fragile IP management.
- Because images are pinned, you avoid version drift.
- Because volumes persist data, you avoid losing history on restart.
Why service names work
When you define a network in Compose, Docker injects a DNS resolver that maps container names to IP addresses. This means you can write:
scrape_configs:
- job_name: 'node'
static_configs:
- targets: ['node-exporter:9100']
And nothing else changes if you recreate containers — the IPs will shift, but Docker updates DNS automatically.
Hands-On Walkthrough
Let's build a complete monitoring stack. You'll need Docker installed (v20.10+ with Compose v2). All files go in one directory, for example monitoring-stack/.
1. Create the Prometheus configuration
Create prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node'
static_configs:
- targets: ['node-exporter:9100']
Pro tip: Use
scrape_interval: 15sfor development; production stacks often use 30s or 1m to reduce load.
2. Write the docker-compose.yml
services:
prometheus:
image: prom/prometheus:v2.50.1
container_name: prometheus
restart: unless-stopped
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
ports:
- '9090:9090'
networks:
- monitoring
node-exporter:
image: prom/node-exporter:v1.7.0
container_name: node-exporter
restart: unless-stopped
command:
- '--path.rootfs=/host'
pid: host
volumes:
- '/:/host:ro,rslave'
networks:
- monitoring
grafana:
image: grafana/grafana:10.4.0
container_name: grafana
restart: unless-stopped
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana_data:/var/lib/grafana
ports:
- '3000:3000'
networks:
- monitoring
networks:
monitoring:
driver: bridge
volumes:
prometheus_data:
grafana_data:
Key points:
networks: monitoring— all containers share the bridge network.- Volumes —
prometheus_dataandgrafana_datapersist across container restarts. - Bind mount —
./prometheus.ymlis mounted read-only, so editing the file and restarting applies changes. - Node Exporter special mode — using
pid: hostand mounting the host root at/hostlets it see host processes and filesystem (with--path.rootfs=/host). Handle with care in production!
3. Start the stack
cd monitoring-stack
docker compose up -d
Expected output:
[+] Running 4/4
✔ Network monitoring-stack_monitoring Created
✔ Volume "monitoring-stack_prometheus_data" Created
✔ Volume "monitoring-stack_grafana_data" Created
✔ Container node-exporter Started
✔ Container prometheus Started
✔ Container grafana Started
4. Verify each piece
# Prometheus targets page
curl http://localhost:9090/targets
# Prometheus API query
curl 'http://localhost:9090/api/v1/query?query=up'
# Grafana login page responds
curl -I http://localhost:3000/login
If everything is healthy, the up query returns 1 for both Prometheus and node-exporter targets.
5. Connect Grafana to Prometheus
- Open
http://localhost:3000and log in withadmin/admin(you'll be prompted to change the password). - Go to Configuration → Data Sources → Add data source.
- Choose Prometheus, set URL to
http://prometheus:9090(service name inside the Docker network). - Click Save & Test — you should see a green success message.
Now you can import a Node Exporter dashboard (ID 1860) from Grafana's community dashboards and see host metrics live.
Compare Options / When to Choose What
| Approach | Pros | Cons | Best for |
|---|---|---|---|
| Docker Compose (single host) | Easy to learn, versioned in Git, reproducible, good for small/medium setups | Not horizontally scalable, no automatic failover, Docker dependency | Dev environments, edge nodes, small production |
| Kubernetes (Prometheus Operator) | Scales, self-healing, service discovery automatic, production-grade | High learning curve, heavier resources, operational overhead | Large multi-node environments, cloud-native stacks |
| Systemd native services | No extra runtime, full control, uses OS package managers | Manual dependency management, IP-based networking, harder to replicate | Minimal hosts, air-gapped systems, performance-critical low-level latency |
When to choose what:
- Docker Compose is the sweet spot for teams that need reproducibility now without hiring a Kubernetes admin.
- Kubernetes is worth it when you have 10+ nodes, or you're already running apps on K8s.
- Systemd remains relevant for edge devices or when you want zero daemon overhead — but you'll spend time on packaging.
Troubleshooting & Edge Cases
Container starts but Prometheus target node-exporter is down
- Check that both containers are on the same network: docker inspect node-exporter --format '{{json .NetworkSettings.Networks}}'
- Confirm service name is spelled correctly (prometheus can resolve it): docker exec prometheus getent hosts node-exporter
- Ensure the exporter is actually listening: docker exec node-exporter wget -qO- http://localhost:9100/metrics | head
Grafana returns 403 when trying to add a Prometheus data source
- Use the service name http://prometheus:9090, not localhost — inside Grafana's container, localhost points to Grafana itself.
Prometheus data disappears after docker compose down and up
- Make sure you didn't use a bind mount for data; named volumes persist only if you don't delete them with docker compose down -v (that flag removes volumes).
Port conflicts on the host
- If 9090 or 3000 are taken, change the left side of ports mapping, e.g., '39090:9090'.
Node Exporter shows only high-level metrics or fails on some filesystems
- Run with --path.rootfs=/host and mount the host root — this is handled, but you might see permission errors if your Docker daemon runs as rootless. Use --no-collector.systemd to skip systemd metrics if they cause noise.
Pro tip: Always log-driver:
docker compose logs prometheusis your best friend; it shows scrape errors, config issues, and authentication problems instantly.
What You Learned & What's Next
You now know how to containerize a monitoring stack with Docker — you can:
- Explain why Docker solves the pain of manual monitoring setup.
- Design a multi-container stack using
docker-compose.ymlwith isolated services, custom bridge networks, and named volumes. - Configure Prometheus to scrape Node Exporter using service-name discovery.
- Connect Grafana to Prometheus inside the Docker network.
- Troubleshoot common container networking, port, and persistence issues.
You've mastered the core workflow of the Linux · networking · telemetry track: turning a set of individual tools into a coherent, reproducible telemetry platform.
Next step: In the next lesson, you'll explore how to dockerize the telemetry pipeline itself — adding an agent that ships logs and traces to the same stack, using the same Compose patterns to unify metrics, logs, and traces into one observable system. You'll also learn to automate stack deployment with CI/CD, ensuring your monitoring stack stays as reproducible as your application code.
Practice recap
Recreate the monitoring stack from this lesson, then modify the Prometheus config to add a third scrape target (e.g., a simple HTTP server you run with docker run). Verify the new target appears in Prometheus's targets page and appears in Grafana. As a challenge, change the Grafana port to 3001 and confirm you can still log in — this tests your port-mapping understanding.
Common mistakes
- Using
localhostinside a container to reach another container — each container has its own network namespace, solocalhostrefers to the container itself. - Forgetting to add all services to the same custom bridge network — they'll be isolated and unable to resolve each other by name.
- Not pinning image versions — using
latestmakes the stack non-reproducible; a change in the image can break your dashboards overnight. - Running
docker compose down -vand losing all Prometheus data — the-vflag deletes named volumes, which is often unintended. - Exposing unnecessary ports to the host (like node-exporter's 9100) — it's a security risk; expose only what you actually need from outside.
Variations
- Kubernetes + Prometheus Operator: replace Compose with Helm charts for automatic service discovery, self-healing, and scaling — best for multi-node clusters.
- Docker stack deploy to Docker Swarm: reuse your
docker-compose.yml(version 3) as a stack definition for multi-node orchestration without Kubernetes complexity. - Systemd native units: skip Docker entirely and run Prometheus, Node Exporter, and Grafana as OS services — useful for minimal hosts where you want zero container overhead.
Real-world use cases
- Spin up a self-hosted Prometheus + Grafana stack on a single VPS to monitor a small web app's metrics and alerts.
- Create a reproducible dev environment where every engineer runs the same monitoring stack via compose without manual installation.
- Run Node Exporter in a container on each edge device and aggregate metrics to a central Prometheus in a Docker network.
Key takeaways
- Docker Compose turns a monitoring stack into a single versioned
docker-compose.ymlfile that's reproducible anywhere. - Containers on a user-defined bridge network communicate with each other using service names, eliminating IP management.
- Use named volumes for persistent data (Prometheus TSDB, Grafana DB) — never assume container filesystems survive restarts.
- Pin exact image versions for all services to keep the stack stable and auditable.
- Expose only necessary ports to the host; keep internal traffic on the private bridge network.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.