# --------------------------------------------------------------------------- # Local development. # # Default (`pnpm infra:up`): backing services only — PostgreSQL, Redis, MinIO, # Mailpit. The three applications run on the host via `pnpm dev`, because # native file watching and HMR are dramatically faster than bind-mounted # containers, especially on macOS. # # Full stack (`docker compose --profile full up`): everything containerised # behind Nginx. This mirrors production topology and is what you use to verify # routing, headers and container builds before shipping. # --------------------------------------------------------------------------- name: sport-store services: postgres: image: postgres:17-alpine container_name: sport-postgres restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER:-sport} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-sport} POSTGRES_DB: ${POSTGRES_DB:-sport_store} # Deterministic collation: index ordering must not depend on the host. POSTGRES_INITDB_ARGS: "--locale=C --encoding=UTF8" ports: - "${POSTGRES_PORT:-5433}:5432" volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-sport} -d ${POSTGRES_DB:-sport_store}"] interval: 5s timeout: 5s retries: 10 redis: image: redis:7-alpine container_name: sport-redis restart: unless-stopped # Cache-first policy: evict rather than refuse writes. Nothing in Redis is # a system of record, so eviction is always preferable to an outage. command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru --appendonly no ports: - "${REDIS_PORT:-6380}:6379" volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 10 # Stands in for Cloudflare R2. Same S3 API, so application code is identical. minio: image: minio/minio:latest container_name: sport-minio restart: unless-stopped command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: ${STORAGE_ACCESS_KEY_ID:-sportminio} MINIO_ROOT_PASSWORD: ${STORAGE_SECRET_ACCESS_KEY:-sportminio} ports: - "9000:9000" - "9001:9001" volumes: - minio-data:/data healthcheck: test: ["CMD", "mc", "ready", "local"] interval: 5s timeout: 5s retries: 10 minio-init: image: minio/mc:latest container_name: sport-minio-init depends_on: minio: condition: service_healthy entrypoint: > /bin/sh -c " mc alias set local http://minio:9000 ${STORAGE_ACCESS_KEY_ID:-sportminio} ${STORAGE_SECRET_ACCESS_KEY:-sportminio}; mc mb --ignore-existing local/${STORAGE_BUCKET:-sport-media}; mc anonymous set download local/${STORAGE_BUCKET:-sport-media}; echo 'MinIO bucket ready'; " # Catches every outgoing email locally. UI on http://localhost:8025 mailpit: image: axllent/mailpit:latest container_name: sport-mailpit restart: unless-stopped ports: - "1025:1025" - "8025:8025" # ------------------------------------------------------------------------ # Application containers — `--profile full` only. # ------------------------------------------------------------------------ api: profiles: ["full"] build: context: . dockerfile: infrastructure/docker/api.Dockerfile container_name: sport-api restart: unless-stopped env_file: apps/api/.env environment: DATABASE_URL: postgresql://${POSTGRES_USER:-sport}:${POSTGRES_PASSWORD:-sport}@postgres:5432/${POSTGRES_DB:-sport_store}?schema=public REDIS_URL: redis://redis:6379 STORAGE_ENDPOINT: http://minio:9000 depends_on: postgres: condition: service_healthy redis: condition: service_healthy expose: - "4000" healthcheck: test: [ "CMD", "node", "-e", "fetch('http://127.0.0.1:4000/api/v1/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", ] interval: 10s timeout: 5s retries: 5 start_period: 20s storefront: profiles: ["full"] build: context: . dockerfile: infrastructure/docker/storefront.Dockerfile args: NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost/api} NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost} container_name: sport-storefront restart: unless-stopped environment: # Server-side rendering talks to the API container directly. API_INTERNAL_URL: http://api:4000 depends_on: api: condition: service_healthy expose: - "3000" admin: profiles: ["full"] build: context: . dockerfile: infrastructure/docker/admin.Dockerfile args: NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost/api} NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_ADMIN_URL:-http://admin.localhost} container_name: sport-admin restart: unless-stopped environment: API_INTERNAL_URL: http://api:4000 depends_on: api: condition: service_healthy expose: - "3001" nginx: profiles: ["full"] image: nginx:1.27-alpine container_name: sport-nginx restart: unless-stopped ports: - "80:80" volumes: - ./infrastructure/nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./infrastructure/nginx/conf.d:/etc/nginx/conf.d:ro depends_on: - storefront - admin - api volumes: postgres-data: redis-data: minio-data: