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"]
Review checklist:
- [ ] Non-root user — container escape should not mean root on the host
- [ ] Slim base — smaller attack surface and faster pulls
- [ ]
HEALTHCHECKhits 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
docker compose up --build
curl http://localhost:8000/health
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
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"),
}
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)