version: "3.9" # A single compose file that supports development and production. # Switch modes by setting APP_ENV and COMPOSE_PROFILES to either # "development" (default) or "production" before running docker compose up. x-backend-common: &backend-common build: context: ./backend dockerfile: Dockerfile env_file: - .env environment: APP_ENV: ${APP_ENV:-development} DATABASE_URL: ${DATABASE_URL} LIVEKIT_URL: ${LIVEKIT_URL} LIVEKIT_API_KEY: ${LIVEKIT_API_KEY} LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET} restart: unless-stopped x-frontend-common: &frontend-common build: context: ./frontend dockerfile: Dockerfile env_file: - .env environment: APP_ENV: ${APP_ENV:-development} # Server-side calls from Next.js hit the backend by container name BACKEND_URL: http://backend:8000 restart: unless-stopped x-postgres-common: &postgres-common image: pgvector/pgvector:pg16 environment: POSTGRES_USER: ${POSTGRES_USER:-postgres} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} POSTGRES_DB: ${POSTGRES_DB:-app} volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-app}"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped x-livekit-common: &livekit-common image: livekit/livekit-server:latest env_file: - .env environment: # Keys are passed in via env; LiveKit will refuse to start without them. LIVEKIT_KEYS: "${LIVEKIT_API_KEY:-devkey}:${LIVEKIT_API_SECRET:-devsecret}" LIVEKIT_PORT: 7880 LIVEKIT_RTC_PORT_RANGE_START: 50000 LIVEKIT_RTC_PORT_RANGE_END: 60000 restart: unless-stopped services: backend-dev: <<: *backend-common profiles: ["development"] container_name: backend command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] ports: - "8000:8000" volumes: # Mount source for hot reload; keep venv inside image - ./backend:/app depends_on: postgres: condition: service_healthy livekit: condition: service_started networks: app_internal: aliases: ["backend"] backend-prod: <<: *backend-common profiles: ["production"] container_name: backend command: - gunicorn - app.main:app - -k - uvicorn.workers.UvicornWorker - --bind - 0.0.0.0:8000 - --workers - "4" - --timeout - "120" expose: - "8000" depends_on: postgres-prod: condition: service_healthy livekit-prod: condition: service_started networks: app_internal: aliases: ["backend"] proxy: aliases: ["backend"] frontend-dev: <<: *frontend-common build: context: ./frontend dockerfile: Dockerfile target: dev profiles: ["development"] container_name: frontend command: ["npm", "run", "dev"] ports: - "3000:3000" volumes: - ./frontend:/app - frontend_node_modules:/app/node_modules depends_on: backend: condition: service_started networks: app_internal: aliases: ["frontend"] frontend-prod: <<: *frontend-common build: context: ./frontend dockerfile: Dockerfile target: runner profiles: ["production"] container_name: frontend # Uses the standalone Next.js output from the Dockerfile command: ["node", "server.js"] expose: - "3000" depends_on: backend-prod: condition: service_started networks: app_internal: aliases: ["frontend"] proxy: aliases: ["frontend"] postgres-dev: <<: *postgres-common profiles: ["development"] container_name: postgres ports: - "5432:5432" networks: app_internal: aliases: ["postgres"] postgres-prod: <<: *postgres-common profiles: ["production"] container_name: postgres networks: app_internal: aliases: ["postgres"] livekit-dev: <<: *livekit-common profiles: ["development"] container_name: livekit ports: - "7880:7880" - "50000-60000:50000-60000/udp" networks: app_internal: aliases: ["livekit"] livekit-prod: <<: *livekit-common profiles: ["production"] container_name: livekit # UDP media must be published even in production; signaling stays internal. ports: - "50000-60000:50000-60000/udp" networks: app_internal: aliases: ["livekit"] proxy: aliases: ["livekit"] volumes: postgres_data: frontend_node_modules: networks: app_internal: # Private app network for service-to-service traffic driver: bridge proxy: # External network provided by the infra stack (Caddy attaches here) external: true