Ctrl K
ComunidadGuías…

docker-compose.prod.yml

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# ============================================================
# FreeFrame — Production Docker Compose
# ============================================================
# Usage:
#   cp .env.example .env.prod        # configure your settings
#   docker compose --env-file .env.prod -f docker-compose.prod.yml up -d --build
#
# SSL: This file alone serves HTTP only (port 80) and requests no
#      certificate. For automatic Let's Encrypt certificates, set DOMAIN
#      and ACME_EMAIL in .env.prod and add -f docker-compose.https.yml.
# ============================================================

services:
  traefik:
    image: traefik:v2.11
    command:
      - --api.dashboard=false
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      # Let's Encrypt resolver; certificates are requested only with -f docker-compose.https.yml
      - --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL:-}
      - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ${DOCKER_SOCK:-/var/run/docker.sock}:/var/run/docker.sock:ro
      - letsencrypt:/letsencrypt
    restart: unless-stopped

  postgres:
    image: postgres:15-alpine
    env_file:
      - .env.prod
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-freeframe}"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    command: >
      sh -c "if [ -n \"$$REDIS_PASSWORD\" ]; then
        redis-server --requirepass \"$$REDIS_PASSWORD\" --appendonly yes;
      else
        redis-server --appendonly yes;
      fi"
    env_file:
      - .env.prod
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD-SHELL", "redis-cli -a \"$${REDIS_PASSWORD}\" --no-auth-warning ping || redis-cli ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  # ─── Optional: self-hosted object storage (Silo, a MinIO fork) ──────────────
  # FreeFrame has NO local-filesystem storage backend — it speaks only the S3 API.
  # By default, point it at external S3-compatible storage (AWS S3, R2, B2, Spaces)
  # via the S3_* vars in .env.prod. To instead keep media on THIS host, run your
  # own S3 server by uncommenting the two services below, then:
  #   1. add `miniodata:` under the top-level `volumes:` (see bottom of file);
  #   2. add `depends_on: { minio: { condition: service_healthy } }` to
  #      api / worker / email_worker / maintenance_worker / beat;
  #   3. in .env.prod set:
  #        S3_STORAGE=minio
  #        S3_ENDPOINT=http://minio:9000
  #        S3_PUBLIC_ENDPOINT=https://storage.${DOMAIN}   # MUST be browser-reachable
  #        S3_ACCESS_KEY / S3_SECRET_KEY = strong values
  #        S3_BUCKET = lowercase letters, digits and hyphens only (no underscores)
  # Presigned URLs are handed to the browser, so the storage server must be
  # reachable from OUTSIDE the compose network — that is what the Traefik router
  # below provides (point a `storage.<your-domain>` DNS record at this host).
  # The bucket can stay private. On startup the app sets the bucket's CORS rules
  # from FRONTEND_URL and CORS_ALLOW_ORIGINS, and Silo enforces them, so
  # FRONTEND_URL must be the exact https:// origin people browse to.
  # An existing miniodata volume from the old minio/minio image opens as is.
  #
  # minio:
  #   image: pgsty/silo:RELEASE.2026-09-16T00-00-00Z   # maintained MinIO fork; minio/minio is no longer pullable (#461)
  #   command: server /data --console-address ":9001"
  #   environment:
  #     MINIO_ROOT_USER: ${S3_ACCESS_KEY}
  #     MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY}
  #   volumes:
  #     - miniodata:/data
  #   healthcheck:
  #     test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
  #     interval: 10s
  #     timeout: 5s
  #     retries: 5
  #   restart: unless-stopped
  #   labels:
  #     - traefik.enable=true
  #     - traefik.http.routers.minio.rule=Host(`storage.${DOMAIN}`)
  #     - traefik.http.routers.minio.entrypoints=websecure
  #     - traefik.http.routers.minio.tls.certresolver=letsencrypt
  #     - traefik.http.services.minio.loadbalancer.server.port=9000
  #
  # minio-init:                     # one-shot: create the bucket (idempotent)
  #   image: pgsty/mc:RELEASE.2026-09-16T00-00-00Z
  #   env_file:
  #     - .env.prod                    # the S3_* vars below are read inside the container
  #   depends_on:
  #     minio:
  #       condition: service_healthy
  #   entrypoint: >
  #     /bin/sh -c "
  #     mc alias set local http://minio:9000 $${S3_ACCESS_KEY} $${S3_SECRET_KEY} &&
  #     mc mb --ignore-existing local/$${S3_BUCKET}
  #     "

  api:
    build:
      context: .
      dockerfile: apps/api/Dockerfile.prod
    env_file:
      - .env.prod
    environment:
      - DISABLE_DOCS=true
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    command: >
      sh -c "cd /workspace/apps/api && alembic upgrade head && cd /workspace &&
      gunicorn apps.api.main:app
      -w $${API_WORKERS:-4}
      -k uvicorn.workers.UvicornWorker
      --bind 0.0.0.0:8000
      --timeout 120
      --graceful-timeout 30
      --access-logfile -"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 5s
      start_period: 30s
      retries: 3
    labels:
      - traefik.enable=true
      # HTTP router (always active)
      - traefik.http.routers.api.rule=PathPrefix(`/api`)
      - traefik.http.routers.api.entrypoints=web
      - traefik.http.routers.api.middlewares=strip-api-prefix
      # HTTPS router (active when SSL is configured)
      - traefik.http.routers.api-secure.rule=PathPrefix(`/api`)
      - traefik.http.routers.api-secure.entrypoints=websecure
      - traefik.http.routers.api-secure.tls.certresolver=letsencrypt
      - traefik.http.routers.api-secure.middlewares=strip-api-prefix
      - traefik.http.routers.api-secure.service=api
      # Shared
      - traefik.http.middlewares.strip-api-prefix.stripprefix.prefixes=/api
      - traefik.http.services.api.loadbalancer.server.port=8000
    restart: unless-stopped

  worker:
    build:
      context: .
      dockerfile: apps/api/Dockerfile.prod
    env_file:
      - .env.prod
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    command: sh -c "celery -A apps.api.tasks.celery_app worker -Q transcoding -c $${TRANSCODING_CONCURRENCY:-2} --loglevel=$${CELERY_LOGLEVEL:-info}"
    restart: unless-stopped

  email_worker:
    build:
      context: .
      dockerfile: apps/api/Dockerfile.prod
    env_file:
      - .env.prod
    depends_on:
      redis:
        condition: service_healthy
    command: sh -c "celery -A apps.api.tasks.celery_app worker -Q email_high,email_low -c $${EMAIL_CONCURRENCY:-2} --loglevel=$${CELERY_LOGLEVEL:-info}"
    restart: unless-stopped

  # Housekeeping: retention GC, stale-upload reaper, orphan sweep. (Watermarking
  # runs on the transcoding worker, see apps/api/tasks/celery_app.py.)
  # Kept off the transcoding worker (these hold a slot for minutes while a video
  # waits) and off the email worker (a magic code queued behind a full bucket
  # listing is a login someone is waiting on).
  maintenance_worker:
    build:
      context: .
      dockerfile: apps/api/Dockerfile.prod
    env_file:
      - .env.prod
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    command: sh -c "celery -A apps.api.tasks.celery_app worker -Q maintenance -c $${MAINTENANCE_CONCURRENCY:-1} --loglevel=$${CELERY_LOGLEVEL:-info}"
    restart: unless-stopped

  beat:
    build:
      context: .
      dockerfile: apps/api/Dockerfile.prod
    env_file:
      - .env.prod
    depends_on:
      redis:
        condition: service_healthy
    command: sh -c "celery -A apps.api.tasks.celery_app beat --loglevel=$${CELERY_LOGLEVEL:-info} -s /tmp/celerybeat-schedule"
    restart: unless-stopped

  web:
    build:
      context: ./apps/web
      dockerfile: Dockerfile.prod
      args:
        NEXT_PUBLIC_API_URL: /api
        NEXT_PUBLIC_BASE_PATH: ${NEXT_PUBLIC_BASE_PATH:-}
    env_file:
      - .env.prod
    environment:
      # Server-to-server, for the branding Next renders into the first byte.
      # It cannot reuse NEXT_PUBLIC_API_URL here: that is `/api`, a path the
      # browser resolves against the current origin and Traefik routes. A fetch
      # from inside this container has no origin to resolve it against.
      - API_INTERNAL_URL=${API_INTERNAL_URL:-http://api:8000}
    depends_on:
      - api
    healthcheck:
      # Probe a public page under the prefix: with NEXT_PUBLIC_BASE_PATH set,
      # the app root `/` answers 404 and the container would never go healthy.
      # Compose interpolates the same value the build arg gets.
      test:
        [
          "CMD-SHELL",
          "wget -q --spider \"http://127.0.0.1:3000${NEXT_PUBLIC_BASE_PATH:-}/login\"",
        ]
      interval: 30s
      timeout: 5s
      start_period: 15s
      retries: 3
    labels:
      - traefik.enable=true
      # HTTP router (always active)
      - traefik.http.routers.web.rule=PathPrefix(`/`)
      - traefik.http.routers.web.entrypoints=web
      - traefik.http.routers.web.priority=1
      # HTTPS router (active when SSL is configured)
      - traefik.http.routers.web-secure.rule=PathPrefix(`/`)
      - traefik.http.routers.web-secure.entrypoints=websecure
      - traefik.http.routers.web-secure.tls.certresolver=letsencrypt
      - traefik.http.routers.web-secure.priority=1
      - traefik.http.routers.web-secure.service=web
      - traefik.http.services.web.loadbalancer.server.port=3000
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:
  letsencrypt:
  # miniodata:      # uncomment if you enable the optional self-hosted MinIO service above