# Requires backend/.env (copied from backend/.env.production.example) — see docs/user-guide/01-getting-started.md ("Building from source"). # For local development use docker-compose.dev.yml instead. services: backend: build: context: . dockerfile: backend/Dockerfile env_file: - backend/.env environment: DATABASE_URL: postgres://openzev:openzev@db:5432/openzev CELERY_BROKER_URL: redis://valkey:6379/0 CACHE_URL: redis://valkey:6379/1 # Only nginx may reach this backend; it overwrites X-Forwarded-For. NUM_PROXIES: 1 volumes: - backend_media:/app/media depends_on: - db - valkey worker: build: context: . dockerfile: backend/Dockerfile command: celery -A config worker -l info env_file: - backend/.env environment: DATABASE_URL: postgres://openzev:openzev@db:5432/openzev CELERY_BROKER_URL: redis://valkey:6379/0 CACHE_URL: redis://valkey:6379/1 volumes: - backend_media:/app/media depends_on: - backend - valkey beat: build: context: . dockerfile: backend/Dockerfile command: celery -A config beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler env_file: - backend/.env environment: DATABASE_URL: postgres://openzev:openzev@db:5432/openzev CELERY_BROKER_URL: redis://valkey:6379/0 CACHE_URL: redis://valkey:6379/1 depends_on: - backend - valkey # On a fresh stack, migrations run inside the backend container's own # startup command and this container can win the race and start before # they land — django_celery_beat's scheduler tables won't exist yet and # it exits. Unlike every other service here, that first failure is # expected rather than a bug, so unlike them beat gets a restart policy. restart: on-failure frontend: build: context: . dockerfile: frontend/Dockerfile args: VITE_API_BASE_URL: /api/v1 NGINX_CONF: nginx.compose.conf # Only a trusted local TLS proxy may reach this port. ports: - "127.0.0.1:8080:8080" depends_on: - backend db: image: postgres:18-alpine environment: POSTGRES_DB: openzev POSTGRES_USER: openzev # Fixed password is deliberate — this port is never published. POSTGRES_PASSWORD: openzev # Pinned rather than inherited: the image default is # /var/lib/postgresql/$PG_MAJOR/docker, so the major version is baked into # the path and the next major bump would point at an empty directory, # initdb over a volume that still holds the old cluster. A subdirectory of # the mount also keeps lost+found out of initdb's way. PGDATA: /var/lib/postgresql/data/pgdata volumes: # docker-compose.dev.yml and docker-compose.fullstack.yml must mount this # volume at the same path — they share the name, and a differing path gives # each stack a cluster the others cannot see (#492). - postgres_data:/var/lib/postgresql/data valkey: image: valkey/valkey:9-alpine volumes: postgres_data: backend_media: