โ† Docker 14 / 18 Next: Caddy & HTTPS โ†’

Docker Compose

Module 5 ยท Infrastructure ยท โฑ ~25 min
Why this matters: JongYang needs three containers to work together: PostgreSQL, FastAPI, and Next.js. Docker Compose defines all three, their relationships, their networks, and how they communicate โ€” in a single file.

The Full docker-compose.yml Annotated

yaml โ€” docker-compose.yml
services:

  # โ”€โ”€ PostgreSQL database โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  postgres:
    image: postgres:16-alpine       # use the official PostgreSQL image
    restart: unless-stopped         # auto-restart if it crashes
    environment:
      POSTGRES_DB: booking_platform
      POSTGRES_USER: ${POSTGRES_USER}           # from .env file
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}   # from .env file
    volumes:
      - postgres_data:/var/lib/postgresql/data  # persist data across restarts
    networks:
      - internal    # ONLY on the internal network โ€” never exposed to Caddy
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s    # check every 10 seconds
      timeout: 5s      # fail if no response within 5 seconds
      retries: 5       # report "healthy" after 5 consecutive successes

  # โ”€โ”€ FastAPI backend โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  fastapi:
    build: ./backend   # build from backend/Dockerfile
    restart: unless-stopped
    environment:
      DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/booking_platform
      # "postgres" hostname resolves to the postgres container on the internal network
      SECRET_KEY: ${SECRET_KEY}
      ALLOWED_ORIGINS: '${ALLOWED_ORIGINS:-["http://localhost:3000"]}'
      # The :-default syntax: use ALLOWED_ORIGINS if set, else use the JSON array default
      OMISE_SECRET_KEY: ${OMISE_SECRET_KEY:-}   # empty string if not set
      LINE_CHANNEL_ACCESS_TOKEN: ${LINE_CHANNEL_ACCESS_TOKEN:-}
    depends_on:
      postgres:
        condition: service_healthy  # wait until postgres passes its health check
    networks:
      - internal    # can talk to postgres
      - caddy-net   # Caddy can route HTTPS requests to it

  # โ”€โ”€ Next.js frontend โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  nextjs:
    build:
      context: ./frontend
      args:               # pass build-time arguments to the Dockerfile
        NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8000}
        NEXT_PUBLIC_BASE_DOMAIN: ${BASE_DOMAIN:-localhost}
    restart: unless-stopped
    networks:
      - caddy-net   # Caddy routes to it โ€” but NOT on internal (no DB access needed)

volumes:
  postgres_data:    # named volume: data persists when containers stop/restart

networks:
  internal:         # private network between postgres and fastapi
    driver: bridge
  caddy-net:
    external: true  # created by the caddy_proxy compose project, shared here

The Two Networks: Security by Design

caddy-net (external, shared):        internal (private):
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ caddy container             โ”‚      โ”‚ fastapi container         โ”‚
โ”‚ nextjs container            โ”‚      โ”‚ postgres container        โ”‚
โ”‚ fastapi container           โ”‚      โ”‚                           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

fastapi is on BOTH networks:
  - caddy-net: so Caddy can reach it
  - internal: so it can reach postgres

nextjs is ONLY on caddy-net:
  - it has no reason to talk to postgres directly
  - keeping it off the internal network prevents accidents

postgres is ONLY on internal:
  - completely unreachable from Caddy or the internet
  - only fastapi can connect to it

depends_on with service_healthy

Without depends_on, Docker might start FastAPI before PostgreSQL is ready to accept connections. The app would crash on startup with "connection refused". The health check solves this:

yaml โ€” health check pattern
healthcheck:
  test: ["CMD-SHELL", "pg_isready -U booking_user"]
  # pg_isready returns exit code 0 if postgres is accepting connections

depends_on:
  postgres:
    condition: service_healthy
  # FastAPI only starts after postgres passes 5 consecutive health checks

Environment Variables and .env

Docker Compose automatically reads a .env file in the same directory. Variables are substituted using ${VAR_NAME}:

bash โ€” .env file format
POSTGRES_USER=booking_user
POSTGRES_PASSWORD=43dded27c39f76af682c2dfb15bc96f484231f103a1548c8
SECRET_KEY=015a0ecab90c365b6a85006c45841eddccca550098a5a59585b1d4e8e39696b8
ALLOWED_ORIGINS=["https://jongyang.xyz","https://admin.jongyang.xyz"]
# Note: ALLOWED_ORIGINS must be JSON array format for pydantic-settings list[str] field

The :-default syntax provides a fallback: ${OMISE_SECRET_KEY:-} means "use OMISE_SECRET_KEY if set, otherwise use empty string". This lets the app start without optional configuration.

Volumes: Persisting PostgreSQL Data

Containers are ephemeral โ€” when they stop, their filesystem changes are lost. PostgreSQL's data files must survive container restarts, so we use a named volume:

bash โ€” volume behavior
# Restart containers โ€” data is preserved (volume survives)
docker compose down
docker compose up -d

# Destroy containers AND volumes โ€” data is GONE (fresh start)
docker compose down -v
# -v flag removes named volumes โ€” USE WITH CAUTION

# Inspect where data is stored
docker volume inspect service-booking-platform_postgres_data

external: true โ€” Sharing Networks Between Compose Projects

Caddy and JongYang are in two separate Docker Compose projects. For Caddy to route to our containers, they must be on the same Docker network. The caddy-net network is created by the caddy project and referenced as external: true in our project:

yaml โ€” caddy_proxy/docker-compose.yaml (caddy project)
networks:
  caddy-net:    # created HERE by the caddy project
    external: false   # (or just omit โ€” this is the default)
yaml โ€” docker-compose.yml (JongYang project)
networks:
  caddy-net:
    external: true    # don't create, just JOIN the existing network

Containers on the same Docker network can reach each other using their container name (or service name) as the hostname. Caddy's config uses reverse_proxy nextjs:3000 โ€” Docker resolves nextjs to the nextjs container's IP on the shared caddy-net.

๐Ÿง  Self-Check Quiz

1. Why is PostgreSQL only on the internal network and not on caddy-net?

Security. Caddy-net is the "public-facing" network that Caddy (and its incoming web traffic) can access. If postgres were on caddy-net, any vulnerability in Caddy or any misconfiguration could potentially expose the database. By keeping postgres only on the internal network, only fastapi can reach it โ€” there's no path from the internet to the database, even accidentally.

2. What does condition: service_healthy in depends_on prevent?

It prevents FastAPI from starting before PostgreSQL is actually ready to accept connections. Without this, Docker might start FastAPI immediately after starting postgres, but postgres takes ~1-2 seconds to initialize. FastAPI would try to connect, fail with "connection refused", and crash. With the health check, FastAPI only starts after postgres passes 5 consecutive pg_isready probes.

3. You set a new LINE_CHANNEL_ACCESS_TOKEN in .env. Do you need to rebuild the Docker image? Why or why not?

No rebuild needed for the FastAPI container. Environment variables set in the environment: section of docker-compose.yml are read at container startup, not at build time. Run docker compose up -d --force-recreate fastapi to restart the container with the new env var. However, NEXT_PUBLIC_* vars for the Next.js frontend ARE baked at build time โ€” those need a rebuild.

4. What happens to PostgreSQL data if you run docker compose down -v?

The named volume postgres_data is deleted, and all database data is permanently lost. The next docker compose up starts with an empty database โ€” you'd need to run migrations (alembic upgrade head) and re-seed the data. docker compose down without -v is safe โ€” it just stops containers, leaves volumes intact.

5. The Caddyfile uses reverse_proxy nextjs:3000. How does Caddy know where "nextjs" is?

Docker's built-in DNS resolver automatically creates hostname entries for all containers on a network. When the nextjs container is on caddy-net, any other container on that same network (including Caddy) can resolve the hostname "nextjs" to the container's IP address. The container name (or service name in the compose project) becomes the DNS hostname within that network.