services: postgres: image: postgres:15-alpine environment: POSTGRES_USER: freeframe POSTGRES_PASSWORD: freeframe POSTGRES_DB: freeframe ports: - "5433:5432" volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U freeframe"] interval: 5s timeout: 5s retries: 5 redis: image: redis:7-alpine ports: - "6379:6379" volumes: - redisdata:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 5s retries: 5 # S3 storage. Silo is a maintained fork of MinIO: the minio/minio and minio/mc # images can no longer be pulled from Docker Hub (#461). It reads the same # MINIO_* variables and opens an existing miniodata volume as is, so the # service keeps its name and S3_ENDPOINT stays http://minio:9000. # Browser CORS is not set here: the API writes the bucket's CORS rules at # startup from FRONTEND_URL and CORS_ALLOW_ORIGINS. minio: image: pgsty/silo:RELEASE.2026-09-16T00-00-00Z command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin ports: - "9000:9000" - "9001:9001" volumes: - miniodata:/data healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 5s timeout: 5s retries: 5 minio-init: image: pgsty/mc:RELEASE.2026-09-16T00-00-00Z depends_on: minio: condition: service_healthy entrypoint: > /bin/sh -c " mc alias set local http://minio:9000 minioadmin minioadmin; mc mb --ignore-existing local/freeframe; mc anonymous set download local/freeframe; exit 0; " api: build: context: . dockerfile: apps/api/Dockerfile ports: - "8000:8000" env_file: - .env environment: DATABASE_URL: postgresql://freeframe:freeframe@postgres:5432/freeframe REDIS_URL: redis://redis:6379/0 # Storage: "minio" for local MinIO (dev), "s3" for AWS S3 (prod) S3_STORAGE: ${S3_STORAGE:-minio} S3_ACCESS_KEY: ${S3_ACCESS_KEY:-minioadmin} S3_SECRET_KEY: ${S3_SECRET_KEY:-minioadmin} S3_ENDPOINT: http://minio:9000 S3_BUCKET: ${S3_BUCKET:-freeframe} S3_REGION: ${S3_REGION:-us-east-1} S3_PUBLIC_ENDPOINT: ${S3_PUBLIC_ENDPOINT:-http://localhost:9000} depends_on: postgres: condition: service_healthy redis: condition: service_healthy minio: condition: service_healthy volumes: - ./apps/api:/workspace/apps/api - ./packages/transcoder:/workspace/packages/transcoder command: > sh -c "cd /workspace/apps/api && alembic upgrade head && cd /workspace && uvicorn apps.api.main:app --host 0.0.0.0 --port 8000 --reload" worker: build: context: . dockerfile: apps/api/Dockerfile env_file: - .env environment: DATABASE_URL: postgresql://freeframe:freeframe@postgres:5432/freeframe REDIS_URL: redis://redis:6379/0 S3_STORAGE: ${S3_STORAGE:-minio} S3_ACCESS_KEY: ${S3_ACCESS_KEY:-minioadmin} S3_SECRET_KEY: ${S3_SECRET_KEY:-minioadmin} S3_ENDPOINT: http://minio:9000 S3_BUCKET: ${S3_BUCKET:-freeframe} S3_REGION: ${S3_REGION:-us-east-1} depends_on: postgres: condition: service_healthy redis: condition: service_healthy minio: condition: service_healthy volumes: - ./apps/api:/workspace/apps/api - ./packages/transcoder:/workspace/packages/transcoder command: celery -A apps.api.tasks.celery_app worker -Q transcoding -c ${TRANSCODING_CONCURRENCY:-2} --loglevel=info email_worker: build: context: . dockerfile: apps/api/Dockerfile env_file: - .env environment: DATABASE_URL: postgresql://freeframe:freeframe@postgres:5432/freeframe REDIS_URL: redis://redis:6379/0 S3_STORAGE: ${S3_STORAGE:-minio} S3_ACCESS_KEY: ${S3_ACCESS_KEY:-minioadmin} S3_SECRET_KEY: ${S3_SECRET_KEY:-minioadmin} S3_ENDPOINT: http://minio:9000 S3_BUCKET: ${S3_BUCKET:-freeframe} S3_REGION: ${S3_REGION:-us-east-1} depends_on: redis: condition: service_healthy volumes: - ./apps/api:/workspace/apps/api - ./packages/transcoder:/workspace/packages/transcoder command: celery -A apps.api.tasks.celery_app worker -Q email_high,email_low -c ${EMAIL_CONCURRENCY:-2} --loglevel=info # Housekeeping worker. Present in dev as well as prod so the periodic tasks # are exercised locally: #240 was invisible for months precisely because dev # ran neither this worker nor beat. # # NOTE: this means your local stack really does delete things on a schedule. # Within the hour, reap_stale_uploads soft-deletes AssetVersions stuck in # uploading/failed past STALE_UPLOAD_TIMEOUT_HOURS and drops their S3 objects # (a failed version whose original is still in storage is kept); # at 03:00 UTC, cleanup_soft_deleted hard-deletes anything soft-deleted past # SOFT_DELETE_RETENTION_DAYS, cascade and S3 reclaim included. Both take `0` # in your .env to switch them off if you keep long-lived local data. maintenance_worker: build: context: . dockerfile: apps/api/Dockerfile env_file: - .env environment: DATABASE_URL: postgresql://freeframe:freeframe@postgres:5432/freeframe REDIS_URL: redis://redis:6379/0 S3_STORAGE: ${S3_STORAGE:-minio} S3_ACCESS_KEY: ${S3_ACCESS_KEY:-minioadmin} S3_SECRET_KEY: ${S3_SECRET_KEY:-minioadmin} S3_ENDPOINT: http://minio:9000 S3_BUCKET: ${S3_BUCKET:-freeframe} S3_REGION: ${S3_REGION:-us-east-1} depends_on: postgres: condition: service_healthy redis: condition: service_healthy minio: condition: service_healthy volumes: - ./apps/api:/workspace/apps/api - ./packages/transcoder:/workspace/packages/transcoder command: celery -A apps.api.tasks.celery_app worker -Q maintenance -c ${MAINTENANCE_CONCURRENCY:-1} --loglevel=info beat: build: context: . dockerfile: apps/api/Dockerfile env_file: - .env environment: DATABASE_URL: postgresql://freeframe:freeframe@postgres:5432/freeframe REDIS_URL: redis://redis:6379/0 S3_STORAGE: ${S3_STORAGE:-minio} S3_ACCESS_KEY: ${S3_ACCESS_KEY:-minioadmin} S3_SECRET_KEY: ${S3_SECRET_KEY:-minioadmin} S3_ENDPOINT: http://minio:9000 S3_BUCKET: ${S3_BUCKET:-freeframe} S3_REGION: ${S3_REGION:-us-east-1} depends_on: redis: condition: service_healthy volumes: - ./apps/api:/workspace/apps/api - ./packages/transcoder:/workspace/packages/transcoder command: celery -A apps.api.tasks.celery_app beat --loglevel=info -s /tmp/celerybeat-schedule web: image: node:20-alpine working_dir: /app ports: - "3000:3000" environment: - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:8000} # Sub-path deployments: empty serves the app from the domain root. - NEXT_PUBLIC_BASE_PATH=${NEXT_PUBLIC_BASE_PATH:-} # Server-to-server, for the branding Next renders into the first byte. # NEXT_PUBLIC_API_URL is written for the browser, and inside this # container `localhost:8000` is this container, not the API. - API_INTERNAL_URL=${API_INTERNAL_URL:-http://api:8000} - NODE_ENV=development - HOSTNAME=0.0.0.0 volumes: - ./apps/web:/app - web_node_modules:/app/node_modules command: sh -c "npm install && npx next dev --hostname 0.0.0.0" depends_on: - api healthcheck: # See docker-compose.prod.yml: `/` is a 404 when a base path is set. test: [ "CMD-SHELL", "wget -q --spider \"http://localhost:3000${NEXT_PUBLIC_BASE_PATH:-}/login\"", ] interval: 30s timeout: 10s retries: 10 start_period: 60s volumes: pgdata: miniodata: redisdata: web_node_modules: