Docker and containers · Intermediate

An application and its monitoring, with Docker Compose

3 min read

Why it matters

Real services have dependencies: a database, a cache, monitoring. Docker Compose describes them in one file and starts them together, which is how most teams run local development environments and integration tests. It is also the gentlest introduction to the ideas Kubernetes uses at scale.

The concept

A compose.yaml file declares:

  • services: each built from a Dockerfile or pulled as an image

  • ports: host to container mappings

  • volumes: persistent or mounted data

  • networks: services reach each other by service name (api:8000)

  • health checks and depends_on conditions, so a service starts only when its dependency is ready

docker compose up -d starts everything, logs -f follows output, down -v removes containers and volumes.

Worked example: fare API plus Prometheus

The API is the instrumented service from Instrument a service: metrics, logs and request ids; the rules file is from SLOs, error budgets and alerts you can test.

layout
stack/
  compose.yaml
  prometheus.yml
  slo-rules.yml
  api/Dockerfile
  api/requirements.txt
  api/service.py
api/Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY service.py .
USER 10001
CMD ["uvicorn", "service:app", "--host", "0.0.0.0", "--port", "8000"]
api/requirements.txt
fastapi==0.142.1
uvicorn[standard]==0.37.0
prometheus-client==0.26.0
prometheus.yml
global:
  scrape_interval: 5s
scrape_configs:
  - job_name: fare-api
    metrics_path: /metrics/
    static_configs:
      - targets: ["api:8000"]
rule_files: ["/etc/prometheus/slo-rules.yml"]
compose.yaml
services:
  api:
    build: ./api
    ports: ["8000:8000"]
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/metrics/')"]
      interval: 10s
      timeout: 3s
      retries: 3

  prometheus:
    image: prom/prometheus:v3.5.0
    ports: ["9090:9090"]
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./slo-rules.yml:/etc/prometheus/slo-rules.yml:ro
      - prometheus-data:/prometheus
    depends_on:
      api:
        condition: service_healthy

volumes:
  prometheus-data:
run.sh
docker compose config --quiet                          # validate the file
docker compose up -d --build
for i in $(seq 1 20); do curl -s "localhost:8000/fare-estimate?miles=$i" > /dev/null; done
curl -s localhost:9090/api/v1/targets | grep -o '"health":"[a-z]*"'      # "health":"up"
docker compose down -v

In a test run, Compose waited for the API to become healthy before starting Prometheus, Prometheus reported the fare-api target as up, counted the requests and loaded all 6 SLO rules. Open localhost:9090 to query rate(http_requests_total[1m]) in the browser.

Prometheus reaches the API as api:8000 because Compose puts both services on a shared network with DNS by service name; localhost inside the Prometheus container would mean Prometheus itself.

Common mistakes

  • localhost between containers. Use the service name.

  • depends_on without a health condition. It only waits for the container to start, not to be ready.

  • Bind-mounting over the app directory in production. Mounts are for development; ship the code inside the image.

  • Secrets in compose.yaml. Use an .env file that is not committed, or Compose secrets.

Interview angle

"How do you run a service and its dependencies locally?" Describe a Compose file with health-checked dependencies, named volumes and service-name networking. Follow-up: "What changes in production?" Orchestration across machines, scaling, rolling updates and secret management: that is where Kubernetes comes in.

Practice task

  1. Add Grafana to the stack with Prometheus as a provisioned data source and a latency panel.

  2. Add a PostgreSQL service with a named volume and make the API wait for it.

  3. Write an integration test that runs docker compose up, calls the API and checks a Prometheus query.

Further reading