After this lesson, you will be able to: Containerize a real app: Next.js + Python API + PostgreSQL behind a Caddy reverse proxy, wired together with Docker Compose.
Time to put it all together. By the end of this lesson, you have a multi-container stack you can `docker compose up` in one command.
Multi-stage, slim runtime, non-root, healthcheck-ready.
# apps/web/DockerfileFROM node:20-alpine AS depsWORKDIR /appCOPY package*.json ./RUN npm ciFROM node:20-alpine AS builderWORKDIR /appCOPY --from=deps /app/node_modules ./node_modulesCOPY . .RUN npm run buildFROM node:20-alpine AS runnerWORKDIR /appENV NODE_ENV=productionRUN addgroup -S app && adduser -S app -G appCOPY --from=builder /app/public ./publicCOPY --from=builder --chown=app:app /app/.next/standalone ./COPY --from=builder --chown=app:app /app/.next/static ./.next/staticUSER appEXPOSE 3000CMD ["node", "server.js"]# Add output: 'standalone' to next.config.js for the .next/standalone path to exist.
Equivalent for Python.
# apps/api/DockerfileFROM python:3.12-slim AS builderWORKDIR /appCOPY requirements.txt .RUN pip install --user --no-cache-dir -r requirements.txtFROM python:3.12-slimWORKDIR /appRUN useradd -m -u 1000 appCOPY --from=builder --chown=app:app /root/.local /home/app/.localCOPY --chown=app:app . .ENV PATH=/home/app/.local/bin:$PATH PYTHONUNBUFFERED=1USER appEXPOSE 8000CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Caddy gives free auto-HTTPS in dev (via /etc/hosts + local CA) and prod (Let's Encrypt).
services:caddy:image: caddy:2-alpineports:- "80:80"- "443:443"volumes:- ./Caddyfile:/etc/caddy/Caddyfile:ro- caddy_data:/data- caddy_config:/configdepends_on:- web- apirestart: unless-stoppedweb:build: ./apps/webenvironment:NEXT_PUBLIC_API_URL: https://api.example.comdepends_on:api:condition: service_healthyrestart: unless-stoppedapi:build: ./apps/apienvironment:DATABASE_URL: postgresql://app:app@db:5432/appdepends_on:db:condition: service_healthyhealthcheck:test: ["CMD", "curl", "-f", "http://localhost:8000/health"]interval: 10stimeout: 5sretries: 5restart: unless-stoppeddb:image: postgres:16-alpineenvironment:POSTGRES_USER: appPOSTGRES_PASSWORD: appPOSTGRES_DB: appvolumes:- pgdata:/var/lib/postgresql/datahealthcheck:test: ["CMD-SHELL", "pg_isready -U app"]interval: 5sretries: 5restart: unless-stoppedvolumes:pgdata:caddy_data:caddy_config:
Put this next to docker-compose.yml.
example.com {reverse_proxy web:3000}api.example.com {reverse_proxy api:8000}# Local dev: replace example.com with localhost; Caddy uses a local CA.# Production: Caddy automatically requests Let's Encrypt certs.
Forgetting `restart: unless-stopped`. After a host reboot, the stack doesn't come back. Healthchecks that never go healthy (the command isn't installed in the slim image). Test from `docker exec`. Reverse-proxying TO the public port. Caddy should talk to web on its internal port 3000, not via a `-p` published port. Mounting source code into production containers (bind mount). Production containers should be immutable. Skipping `read_only: true` for stateless services. The base image filesystem doesn't need to be writable.
Pick the most senior reason.
Sign in and purchase access to unlock this lesson.