Docker and containers · Intermediate
An application and its monitoring, with Docker Compose
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_onconditions, 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.
stack/
compose.yaml
prometheus.yml
slo-rules.yml
api/Dockerfile
api/requirements.txt
api/service.pyFROM 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"]fastapi==0.142.1
uvicorn[standard]==0.37.0
prometheus-client==0.26.0global:
scrape_interval: 5s
scrape_configs:
- job_name: fare-api
metrics_path: /metrics/
static_configs:
- targets: ["api:8000"]
rule_files: ["/etc/prometheus/slo-rules.yml"]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: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 -vIn 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
localhostbetween containers. Use the service name.depends_onwithout 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.envfile 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
Add Grafana to the stack with Prometheus as a provisioned data source and a latency panel.
Add a PostgreSQL service with a named volume and make the API wait for it.
Write an integration test that runs
docker compose up, calls the API and checks a Prometheus query.