Docker Compose Deep Dive and Service Health
Docker Compose Deep Dive and Service Health
The Feature
A developer runs docker compose up and every service starts in the correct order, waits for its dependencies to be healthy, and provides hot reload for both backend and frontend code changes.
The Decision
Docker Compose’s depends_on with condition: service_healthy ensures services start in the right order. Without health checks, Docker Compose starts services in dependency order but does not wait for them to be ready. The backend starts before PostgreSQL has finished initializing and crashes with a connection refused error. Health checks eliminate this race condition.
The Implementation
Backend Dockerfile
# backend/Dockerfile
FROM python:3.12-slim AS base
WORKDIR /app
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
# Copy dependency files
COPY pyproject.toml uv.lock ./
# Install dependencies
RUN uv sync --frozen --no-dev
# Copy application code
COPY . .
# Development target: includes dev dependencies and uses reload
FROM base AS development
RUN uv sync --frozen
CMD ["uv", "run", "uvicorn", "app.main:app", "--reload", "--host", "0.0.0.0", "--port", "8000"]
# Production target: minimal image
FROM base AS production
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
The multi-stage build provides two targets. Development includes dev dependencies (pytest, ruff) and runs with --reload. Production excludes dev dependencies and runs with multiple workers. The same Dockerfile serves both environments.
Frontend Dockerfile
# frontend/Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
The frontend Dockerfile is minimal because the frontend is a static build in production (served by Cloudflare or a simple nginx container). In development, it runs the Vite dev server.
Debugging Docker Compose
# View logs for a specific service
docker compose logs -f backend
# Rebuild a single service after changing Dockerfile
docker compose build backend
# Shell into a running container
docker compose exec backend bash
# Check health status of all services
docker compose ps
# Reset everything: stop, remove volumes, rebuild
docker compose down -v && docker compose up --build
# Run alembic migrations inside the backend container
docker compose exec backend uv run alembic upgrade head
# Run tests inside the backend container
docker compose exec backend uv run pytest
The docker compose exec command runs inside an already-running container. The docker compose run command starts a new container. For one-off commands like migrations and tests, exec is faster because it does not start a new container.
The Trap
# TRAP: Volume mount that shadows installed node_modules
services:
frontend:
volumes:
- ./frontend:/app
# npm packages installed during build are overwritten by the
# host's ./frontend directory, which may not have node_modules
# SAFE: Anonymous volume preserves container's node_modules
services:
frontend:
volumes:
- ./frontend:/app
- /app/node_modules # Anonymous volume prevents host from overwriting
The anonymous volume /app/node_modules tells Docker to keep the container’s node_modules directory instead of mounting the host’s. Without this, the host’s ./frontend directory (which may not have node_modules) overwrites the container’s installed packages.
The Cost
Docker Desktop is free for personal use and small businesses. OrbStack (macOS alternative) is free for personal use. The disk space for images and volumes is typically 2-5 GB for this stack. The memory overhead of running PostgreSQL, Redis, the backend, and the frontend in Docker is approximately 1 GB.