Docker Compose
The Full docker-compose.yml Annotated
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:
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}:
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:
# 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:
networks:
caddy-net: # created HERE by the caddy project
external: false # (or just omit โ this is the default)
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?
2. What does condition: service_healthy in depends_on prevent?
3. You set a new LINE_CHANNEL_ACCESS_TOKEN in .env. Do you need to rebuild the Docker image? Why or why not?
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?
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?