DEV Community

Muhammad Umair Virk
Muhammad Umair Virk

Posted on

Docker Compose for Python Backends — A Minimal Production Stack

Most Python backends eventually leave "run on my laptop." The first container deploy is where teams discover that localhost is not a universal address and that a Dockerfile without a health check is a black box to the load balancer.

This is the minimal stack I use and review in PRs: FastAPI, a hardened Dockerfile, Compose for local parity with production topology.

Companion article on Medium: Why Serverless Engineers Already Understand Containers — same patterns, more on mental models.


1. Dockerfile essentials

FROM python:3.12-slim

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

RUN useradd --create-home appuser
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app

USER appuser
EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')"

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Enter fullscreen mode Exit fullscreen mode

Review checklist:

  • [ ] Non-root user — container escape should not mean root on the host
  • [ ] Slim base — smaller attack surface and faster pulls
  • [ ] HEALTHCHECK hits a real endpoint that verifies dependencies if possible
  • [ ] No secrets in layers — use env injection at runtime

2. docker-compose.yml

services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      ENVIRONMENT: local-compose
Enter fullscreen mode Exit fullscreen mode
docker compose up --build
curl http://localhost:8000/health
Enter fullscreen mode Exit fullscreen mode

If /health returns a hostname that changes between restarts, you are talking to the container — not a stale local process.


3. The rule that prevents the most outages

Inside Compose, localhost is the container itself.

When you add Postgres:

services:
  api:
    build: .
    environment:
      DATABASE_URL: postgresql://app:secret@db:5432/appdb
    depends_on:
      - db
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: appdb
Enter fullscreen mode Exit fullscreen mode

The API must use host db, not 127.0.0.1. This single mistake causes more first-week container incidents than any Dockerfile syntax error.


4. Health endpoint

import os
import socket
from fastapi import APIRouter

router = APIRouter()

@router.get("/health")
def health():
    return {
        "status": "healthy",
        "hostname": socket.gethostname(),
        "environment": os.getenv("ENVIRONMENT", "development"),
    }
Enter fullscreen mode Exit fullscreen mode

Orchestrators, load balancers, and Compose use this to decide whether to route traffic. A listening socket that cannot serve requests should fail the check.


5. Local vs. production gaps

Concern Local Compose Production
Secrets .env (gitignored) Secrets Manager / platform env
Database Named volume Managed RDS + migrations
Logs stdout Structured JSON → aggregation
Deploy Manual CI pipeline → registry → roll out

Closing these gaps deliberately — not on the day of launch — is what separates a demo from a service.


Further reading


Muhammad Umair Virk — Backend Engineer, UAE. Python · AWS · microservices · payments.

Top comments (0)