2026-10-07 19:40:26 +03:30
# Shared services — always combine with compose.dev.yaml or compose.prod.yaml:
#
# Dev: docker compose -f deploy/compose/compose.yaml -f deploy/compose/compose.dev.yaml up -d --build
# Prod: docker compose -f deploy/compose/compose.yaml -f deploy/compose/compose.prod.yaml --env-file .env.prod up -d --build
#
# Or use scripts/compose-dev.sh / scripts/compose-prod.sh / root docker-compose.yml (dev default).
name : pargar
x-backend-env : &backend-env
PORT : "8080"
DATABASE_URL : postgres://${POSTGRES_USER:-pargar}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}@postgres:5432/${POSTGRES_DB:-pargar}?sslmode=disable
REDIS_URL : redis://:${REDIS_PASSWORD:?REDIS_PASSWORD must be set}@redis:6379
JWT_SECRET : ${JWT_SECRET:?JWT_SECRET must be set}
PUBLIC_URL : ${PUBLIC_URL:?PUBLIC_URL must be set}
FRONTEND_ORIGINS : ${FRONTEND_ORIGINS:?FRONTEND_ORIGINS must be set}
METRICS_TOKEN : ${METRICS_TOKEN:-}
TRUSTED_PROXY_CIDRS : ${TRUSTED_PROXY_CIDRS:-}
STORAGE_DRIVER : s3
STORAGE_ENDPOINT : http://rustfs:9000
STORAGE_REGION : ${STORAGE_REGION:-us-east-1}
STORAGE_ACCESS_KEY : ${RUSTFS_ACCESS_KEY:?RUSTFS_ACCESS_KEY must be set}
STORAGE_SECRET_KEY : ${RUSTFS_SECRET_KEY:?RUSTFS_SECRET_KEY must be set}
STORAGE_BUCKET : ${STORAGE_BUCKET:-pargar}
SEED_CURRICULUM : ${SEED_CURRICULUM:-false}
2026-10-07 23:13:22 +03:30
REGISTRATION_ENFORCE_IP : ${REGISTRATION_ENFORCE_IP:-false}
2026-10-07 19:40:26 +03:30
ADMIN_NAME : ${ADMIN_NAME:-Admin}
ADMIN_USERNAME : ${ADMIN_USERNAME:-admin}
ADMIN_EMAIL : ${ADMIN_EMAIL:-admin@example.com}
ADMIN_PASSWORD : ${ADMIN_PASSWORD:?ADMIN_PASSWORD must be set}
OTEL_EXPORTER_OTLP_ENDPOINT : ${OTEL_EXPORTER_OTLP_ENDPOINT:-}
OTEL_SERVICE_NAME : ${OTEL_SERVICE_NAME:-pargar-api}
DONATION_ENABLED : ${DONATION_ENABLED:-true}
DONATION_NOTE : ${DONATION_NOTE:-}
PHYSICAL_CERT_ENABLED : ${PHYSICAL_CERT_ENABLED:-true}
PHYSICAL_CERT_PRICE_IRR : ${PHYSICAL_CERT_PRICE_IRR:-2500000}
PHYSICAL_CERT_WINDOW_DAYS : ${PHYSICAL_CERT_WINDOW_DAYS:-30}
services :
postgres :
image : postgres:16.4-alpine
restart : unless-stopped
environment :
POSTGRES_USER : ${POSTGRES_USER:-pargar}
POSTGRES_PASSWORD : ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}
POSTGRES_DB : ${POSTGRES_DB:-pargar}
volumes :
- pgdata:/var/lib/postgresql/data
healthcheck :
test : [ "CMD-SHELL" , "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\"" ]
interval : 5s
timeout : 3s
retries : 20
networks :
- pargar
redis :
image : redis@sha256:858f009f9709ce576febc734aa78b8f6d624b82571f9ddb6bda4377c833b3499
restart : unless-stopped
command : [ "redis-server" , "--requirepass" , "${REDIS_PASSWORD:?REDIS_PASSWORD must be set}" , "--appendonly" , "yes" ]
volumes :
- redis_data:/data
healthcheck :
test : [ "CMD" , "redis-cli" , "-a" , "${REDIS_PASSWORD:?REDIS_PASSWORD must be set}" , "ping" ]
interval : 5s
timeout : 3s
retries : 20
networks :
- pargar
rustfs :
image : rustfs/rustfs@sha256:1803faef57627e2d9c2e7d89d655d712ddded5389040054987163043fecb6a3c
restart : unless-stopped
command : /data
environment :
RUSTFS_ACCESS_KEY : ${RUSTFS_ACCESS_KEY:?RUSTFS_ACCESS_KEY must be set}
RUSTFS_SECRET_KEY : ${RUSTFS_SECRET_KEY:?RUSTFS_SECRET_KEY must be set}
RUSTFS_ADDRESS : ":9000"
RUSTFS_CONSOLE_ADDRESS : ":9001"
RUSTFS_CONSOLE_ENABLE : ${RUSTFS_CONSOLE_ENABLE:-false}
volumes :
- rustfs_data:/data
healthcheck :
test : [ "CMD" , "wget" , "--spider" , "--quiet" , "http://localhost:9000/health" ]
interval : 5s
timeout : 3s
retries : 20
start_period : 10s
networks :
- pargar
backend :
build :
context : ../../backend
dockerfile : Dockerfile
image : ${BACKEND_IMAGE:-pargar-backend:local}
restart : unless-stopped
environment :
<< : *backend-env
DEV_MODE : ${DEV_MODE:-false}
depends_on :
postgres :
condition : service_healthy
redis :
condition : service_healthy
rustfs :
condition : service_healthy
expose :
- "8080"
healthcheck :
test : [ "CMD" , "wget" , "--spider" , "--quiet" , "http://127.0.0.1:8080/api/health" ]
interval : 5s
timeout : 3s
retries : 20
networks :
- pargar
labels :
- traefik.enable=true
- traefik.docker.network=pargar_pargar
# API + media (higher priority than frontend catch-all)
- traefik.http.routers.pargar-api.rule=Host(`${DOMAIN:-localhost}`) && (PathPrefix(`/api`) || PathPrefix(`/media`))
- traefik.http.routers.pargar-api.entrypoints=websecure
- traefik.http.routers.pargar-api.priority=100
- traefik.http.routers.pargar-api.tls=true
- traefik.http.routers.pargar-api.middlewares=security-headers@file,compress@file
- traefik.http.routers.pargar-api.service=pargar-api
- traefik.http.services.pargar-api.loadbalancer.server.port=8080
# Certificate public API — noindex
- traefik.http.routers.pargar-cert-api.rule=Host(`${DOMAIN:-localhost}`) && PathPrefix(`/api/certificates`)
- traefik.http.routers.pargar-cert-api.entrypoints=websecure
- traefik.http.routers.pargar-cert-api.priority=110
- traefik.http.routers.pargar-cert-api.tls=true
- traefik.http.routers.pargar-cert-api.middlewares=security-headers@file,compress@file,noindex@file
- traefik.http.routers.pargar-cert-api.service=pargar-api
frontend :
build :
context : ../../frontend
dockerfile : Dockerfile
args :
NEXT_PUBLIC_API_URL : ""
NEXT_PUBLIC_WS_URL : ""
image : ${FRONTEND_IMAGE:-pargar-frontend:local}
restart : unless-stopped
environment :
NODE_ENV : production
depends_on :
backend :
condition : service_healthy
expose :
- "3000"
healthcheck :
test : [ "CMD" , "wget" , "--spider" , "--quiet" , "http://127.0.0.1:3000" ]
interval : 10s
timeout : 3s
retries : 10
networks :
- pargar
labels :
- traefik.enable=true
- traefik.docker.network=pargar_pargar
- traefik.http.routers.pargar-web.rule=Host(`${DOMAIN:-localhost}`)
- traefik.http.routers.pargar-web.entrypoints=websecure
- traefik.http.routers.pargar-web.priority=1
- traefik.http.routers.pargar-web.tls=true
- traefik.http.routers.pargar-web.middlewares=security-headers@file,compress@file
- traefik.http.routers.pargar-web.service=pargar-web
- traefik.http.services.pargar-web.loadbalancer.server.port=3000
# Unlisted certificate pages
- traefik.http.routers.pargar-cert-web.rule=Host(`${DOMAIN:-localhost}`) && PathPrefix(`/c`)
- traefik.http.routers.pargar-cert-web.entrypoints=websecure
- traefik.http.routers.pargar-cert-web.priority=50
- traefik.http.routers.pargar-cert-web.tls=true
- traefik.http.routers.pargar-cert-web.middlewares=security-headers@file,compress@file,noindex@file
- traefik.http.routers.pargar-cert-web.service=pargar-web
networks :
pargar :
name : pargar_pargar
volumes :
pgdata :
redis_data :
rustfs_data :