Docker¶
Philosophy¶
Two Compose files for full environment parity:
docker-compose.yml— development: volume mounts, live reload, no PostgreSQL neededdocker-compose.prod.yml— production: built image, Uvicorn workers, PostgreSQL, Caddy
The application code is identical in both — only the infrastructure layer differs.
Dockerfile¶
FROM python:3.14-slim
# Install uv from its official image (fast, no pip needed)
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app
# Install dependencies first — Docker caches this layer
# Only re-runs if pyproject.toml or uv.lock changes
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
# Copy application code
COPY . .
# Compile translation files
RUN uv run python manage.py compilemessages
# Collect static files into staticfiles/
RUN uv run python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["uv", "run", "uvicorn", "config.asgi:application",
"--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
Note
--frozen on uv sync means: fail if uv.lock is out of sync with pyproject.toml.
This prevents silent dependency drift in production images.
docker-compose.yml (development)¶
services:
web:
build: .
command: uv run uvicorn config.asgi:application --reload --host 0.0.0.0 --port 8000
volumes:
- .:/app # mount source code — changes reflect without rebuild
ports:
- "8000:8000"
env_file:
- .env
No PostgreSQL in dev — SQLite file lives at db.sqlite3 in the mounted volume.
docker-compose.prod.yml (production)¶
services:
web:
build: .
restart: always
env_file: .env
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
restart: always
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 5s
retries: 5
caddy:
image: caddy:2-alpine
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
volumes:
postgres_data:
caddy_data:
caddy_config:
Useful Commands¶
# Development
docker compose up # start dev stack
docker compose exec web uv run python manage.py migrate
docker compose exec web uv run python manage.py createsuperuser
# Production
docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml exec web uv run python manage.py migrate
docker compose -f docker-compose.prod.yml logs -f web
# Rebuild after code changes (prod)
docker compose -f docker-compose.prod.yml build
docker compose -f docker-compose.prod.yml up -d