Skip to content
ExponaBLOG
PROMETHEUS, GRAFANA & MONITORING · DAY 004 / 100

Running Prometheus with Docker for Monitoring developers

A local setup in minutes.

Expona Admin7 min read

Getting started with Docker-based Prometheus

Running Prometheus with Docker for monitoring developers reduces setup friction from hours to minutes. Instead of managing binaries, dependencies, and system configurations, you spin up a container, drop in a config file, and start collecting metrics. This approach scales beautifully across your local environment, CI/CD pipelines, and staging clusters.

In this guide, we'll walk through setting up Prometheus in Docker, configuring scrape targets, exploring the query interface, and extending the stack with Grafana and exporters. By the end, you'll have a monitoring foundation that mirrors what runs in production—without the operational overhead.

Why Docker matters for monitoring setup

Docker eliminates version mismatches and platform drift. A developer on macOS, another on Linux, and a CI runner all execute identical Prometheus code inside the same container image. No "works on my machine" surprises when metrics stop flowing. You also gain instant repeatability: tear down and rebuild your entire monitoring stack in seconds.

For teams using Kubernetes or container orchestration, Docker-based Prometheus becomes your local test bed. You validate scrape configs, alert rules, and Grafana dashboards before pushing to staging. The muscle memory transfers directly: a docker-compose file is nearly identical to a Kubernetes StatefulSet configuration in terms of intent.

Step 1: Creating a Prometheus configuration file

Prometheus reads a YAML configuration file that defines global settings and scrape targets. This file is the single source of truth for what your monitoring system observes. Let's build a practical config that targets a local API server.

yaml
global:
  scrape_interval: 15s
  scrape_timeout: 10s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'api'
    static_configs:
      - targets: ['api:3000']
        labels:
          environment: 'local'
          
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

What each section does:

  • global sets defaults for all jobs. scrape_interval: 15s means Prometheus pulls metrics every 15 seconds from each target.
  • scrape_timeout: 10s terminates slow scrapes after 10 seconds to prevent hanging.
  • scrape_configs lists all monitoring targets. The first job targets your API on port 3000; the second monitors Prometheus itself.
  • static_configs pairs target addresses with optional labels. Labels help you filter and organize metrics in queries.

Save this file as prometheus.yml in your project root. When you run the container, you'll mount this file into the Prometheus container so it reads your configuration on startup.

A real-world scenario: your team develops a NestJS API that exposes Prometheus metrics on /metrics at port 3000. Your microservice orchestrator (Docker Compose, for now) runs it as a service named api. Prometheus uses Docker's internal DNS to resolve api:3000 and scrapes metrics every 15 seconds. No hardcoded IPs; the container network handles service discovery.

Step 2: Running Prometheus in Docker

With your config file ready, launch Prometheus using the official image:

yaml
docker run \
  -p 9090:9090 \
  -v ./prometheus.yml:/etc/prometheus/prometheus.yml \
  prom/prometheus:latest

Breaking down the command:

  • -p 9090:9090 maps the container's port 9090 (Prometheus default) to your host, so you access the UI at localhost:9090.
  • -v ./prometheus.yml:/etc/prometheus/prometheus.yml binds your local config file into the container. Any changes you save locally are immediately visible inside the container on the next scrape cycle (no restart required for config reloads in some cases).
  • prom/prometheus:latest specifies the official Prometheus image.

Run this command in your terminal. Within a few seconds, you'll see Prometheus start up and begin scraping. Check the logs—they'll show:

yaml
msg="Server is ready to receive web requests."

If your API isn't running yet, Prometheus won't fail; it logs a scrape error and retries after 15 seconds. This resilience is intentional: monitoring systems must tolerate brief target downtime.

To verify startup, check the Prometheus UI at http://localhost:9090. You should see the web interface with the graph explorer and status pages.

Step 3: Exploring metrics with the Prometheus UI

The Prometheus web UI is your playground for understanding what metrics are available and how to query them. Navigate to http://localhost:9090/graph to open the expression browser.

Start with the simplest possible query: up. This built-in metric tracks scrape status for each job. Enter it in the text box and click Execute.

Interpreting the result: Prometheus returns a table with rows for each scrape target. If your API is running, you'll see:

yaml
up{instance="api:3000", job="api"}  1
up{instance="localhost:9090", job="prometheus"}  1

A value of 1 means the target is healthy and reachable. 0 means the scrape failed. This single metric is invaluable for alerting: if up == 0, your target is down.

Next, try querying actual metrics from your API. If you're running a Node.js or .NET app with Prometheus instrumentation, common metrics include:

  • http_requests_total — cumulative HTTP request count by method and status.
  • http_request_duration_seconds — histogram of request latencies.
  • process_resident_memory_bytes — process memory usage.

Query rate(http_requests_total[5m]) to see the request rate over the last 5 minutes. Prometheus calculates the derivative automatically. This pattern—rate() over a time window—is foundational for alerting on trends rather than absolute values.

Use the Graph tab to visualize metrics over time. Zoom, pan, and inspect individual data points. This interactive exploration builds your intuition for metric behavior before you commit to dashboards and alerts.

Step 4: Building a multi-service stack with Docker Compose

Running Prometheus alone is useful; adding Grafana and exporters creates a complete observability platform. Docker Compose orchestrates multiple services with shared networking, volumes, and environment variables—all in one declarative file.

yaml
version: '3.8'

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
    networks:
      - monitoring
    depends_on:
      - api

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana_data:/var/lib/grafana
    networks:
      - monitoring
    depends_on:
      - prometheus

  api:
    build: ./api
    container_name: api
    ports:
      - "8080:3000"
    networks:
      - monitoring
    environment:
      - NODE_ENV=development

volumes:
  prometheus_data:
  grafana_data:

networks:
  monitoring:
    driver: bridge

Key features of this setup:

  • volumes at the service level persist data across container restarts. prometheus_data stores time-series data; grafana_data stores dashboards and configurations.
  • networks create an isolated network called monitoring where all services resolve each other by name. Prometheus talks to api:3000 without exposing it to your host.
  • depends_on ensures startup order, though it doesn't guarantee readiness. (A more robust approach uses health checks, but this is sufficient for local development.)
  • environment configures service behavior. Grafana's default admin password is set to admin; change this in production.

Start the stack with:

yaml
docker-compose up -d

This builds and runs all three services in the background. Check status with:

yaml
docker-compose ps

Access Grafana at localhost:3000 (user: admin, password: admin). Add Prometheus as a data source pointing to http://prometheus:9090, then create dashboards that visualize your API's metrics.

When you're done, shut down cleanly with:

yaml
docker-compose down

Your volumes persist, so next time you run docker-compose up, historical data and dashboards are restored.

Adding exporters for system-level monitoring

Exporters bridge the gap between applications that don't expose Prometheus metrics natively and Prometheus itself. The Node Exporter collects CPU, memory, disk, and network metrics from Linux systems. Add it to your compose file:

yaml
  node-exporter:
    image: prom/node-exporter:latest
    container_name: node-exporter
    ports:
      - "9100:9100"
    networks:
      - monitoring
    command:
      - '--path.rootfs=/'
      - '--path.procfs=/proc'
      - '--path.sysfs=/sys'

Then update your Prometheus config to scrape it:

yaml
  - job_name: 'node'
    static_configs:
      - targets: ['node-exporter:9100']

Reload Prometheus, and you'll immediately see metrics like node_cpu_seconds_total, node_memory_MemAvailable_bytes, and node_disk_io_reads_completed_total. These complement application-level metrics and help diagnose resource contention.

Common mistakes

  • Mounting the config file at the wrong path: Always mount to /etc/prometheus/prometheus.yml. Prometheus looks for this path by default. Mounting to the wrong location causes silent failures.
  • Forgetting the networking section: If services are on different networks, they can't resolve each other by name. Docker Compose creates a bridge network by default, but be explicit to avoid confusion.
  • Using localhost inside containers: A container's localhost is its own loopback interface, not your host machine. Service-to-service communication uses service names (e.g., prometheus:9090, not localhost:9090).
  • Not persisting data: If you don't attach volumes, restarting containers loses all historical metrics and Grafana dashboards. Always define persistent volumes for stateful services.
  • Setting scrape intervals too low: A 5-second interval generates four times more data than a 20-second interval. Start with 15-30 seconds and adjust based on query needs, not on perceived urgency.
  • Ignoring resource limits: In production, Prometheus can consume significant memory. Docker Compose doesn't set limits by default. Add mem_limit to prevent a runaway Prometheus from starving other services.

Key takeaways

  • Docker eliminates configuration drift and makes monitoring setup reproducible across your team and CI/CD pipelines.
  • A Prometheus configuration file is a simple YAML document that defines global settings and scrape targets; mount it into the container to customize behavior.
  • The Prometheus web UI is a powerful exploration tool—use up to check target health and rate() functions to derive actionable signals from raw metrics.
  • Docker Compose orchestrates multi-service stacks with shared networking and persistent storage, mirroring production-like setups locally.
  • Service-to-service communication inside containers uses DNS names, not localhost; rely on the service name defined in compose to resolve addresses.
  • Start with basic metrics (up, request rate, latency histograms) and expand your observability as your team's needs grow. Monitoring is iterative, not a one-time setup.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles