name: ${COMPOSE_PROJECT_NAME:-frameflow-prod} x-server-image: &server-image image: frameflow-server:${FRAMEFLOW_VERSION:-latest} build: context: . dockerfile: Dockerfile.server x-server-environment: &server-environment NODE_ENV: production API_HOST: 0.0.0.0 API_PORT: 8787 API_PUBLIC_ORIGIN: ${API_PUBLIC_ORIGIN:?Set API_PUBLIC_ORIGIN} WEB_ORIGIN: ${WEB_ORIGIN:?Set WEB_ORIGIN} LOG_LEVEL: ${LOG_LEVEL:-info} METRICS_HOST: 0.0.0.0 WORKER_METRICS_PORT: 9092 WORKER_HEARTBEAT_KEY: ${WORKER_HEARTBEAT_KEY:-frameflow:worker:heartbeat} WORKER_HEARTBEAT_INTERVAL_MS: ${WORKER_HEARTBEAT_INTERVAL_MS:-5000} WORKER_HEARTBEAT_TTL_SECONDS: ${WORKER_HEARTBEAT_TTL_SECONDS:-20} METRICS_TOKEN: ${METRICS_TOKEN:-} DATABASE_URL: ${DATABASE_URL:?Set DATABASE_URL} REDIS_URL: ${REDIS_URL:?Set REDIS_URL} JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET:?Set JWT_ACCESS_SECRET} JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:?Set JWT_REFRESH_SECRET} CREDENTIAL_ENCRYPTION_KEY: ${CREDENTIAL_ENCRYPTION_KEY:?Set CREDENTIAL_ENCRYPTION_KEY} ACCESS_TOKEN_TTL: ${ACCESS_TOKEN_TTL:-15m} REFRESH_TOKEN_TTL_DAYS: ${REFRESH_TOKEN_TTL_DAYS:-30} EMAIL_VERIFICATION_REQUIRED: ${EMAIL_VERIFICATION_REQUIRED:-true} EMAIL_VERIFICATION_TTL_HOURS: ${EMAIL_VERIFICATION_TTL_HOURS:-24} PASSWORD_RESET_TTL_MINUTES: ${PASSWORD_RESET_TTL_MINUTES:-30} RESEND_API_KEY: ${RESEND_API_KEY:?Set RESEND_API_KEY} EMAIL_FROM: ${EMAIL_FROM:?Set EMAIL_FROM} S3_ENDPOINT: http://minio:9000 S3_PUBLIC_ENDPOINT: ${S3_PUBLIC_ENDPOINT:?Set S3_PUBLIC_ENDPOINT} S3_REGION: ${S3_REGION:-us-east-1} S3_BUCKET: ${S3_BUCKET:-frameflow} S3_ACCESS_KEY: ${S3_ACCESS_KEY:?Set S3_ACCESS_KEY} S3_SECRET_KEY: ${S3_SECRET_KEY:?Set S3_SECRET_KEY} S3_FORCE_PATH_STYLE: "true" OPENAI_API_KEY: ${OPENAI_API_KEY:-} OPENAI_TEXT_MODEL: ${OPENAI_TEXT_MODEL:-gpt-5.6-sol} OPENAI_IMAGE_MODEL: ${OPENAI_IMAGE_MODEL:-gpt-image-2} OPENAI_TTS_MODEL: ${OPENAI_TTS_MODEL:-gpt-4o-mini-tts} OPENAI_TTS_VOICE: ${OPENAI_TTS_VOICE:-alloy} AGNES_API_KEY: ${AGNES_API_KEY:-} AGNES_TEXT_MODEL: ${AGNES_TEXT_MODEL:-agnes-2.5-flash} AGNES_IMAGE_MODEL: ${AGNES_IMAGE_MODEL:-agnes-image-2.1-flash} AGNES_VIDEO_MODEL: ${AGNES_VIDEO_MODEL:-agnes-video-v2.0} AGNES_VIDEO_PREDICTION_DEADLINE_SECONDS: ${AGNES_VIDEO_PREDICTION_DEADLINE_SECONDS:-900} AGNES_VIDEO_POLL_INTERVAL_MS: ${AGNES_VIDEO_POLL_INTERVAL_MS:-4000} GENERIC_TEXT_API_KEY: ${GENERIC_TEXT_API_KEY:-} GENERIC_TEXT_BASE_URL: ${GENERIC_TEXT_BASE_URL:-} GENERIC_TEXT_MODEL: ${GENERIC_TEXT_MODEL:-} GENERIC_IMAGE_API_KEY: ${GENERIC_IMAGE_API_KEY:-} GENERIC_IMAGE_BASE_URL: ${GENERIC_IMAGE_BASE_URL:-} GENERIC_IMAGE_MODEL: ${GENERIC_IMAGE_MODEL:-} GENERIC_VIDEO_API_KEY: ${GENERIC_VIDEO_API_KEY:-} GENERIC_VIDEO_BASE_URL: ${GENERIC_VIDEO_BASE_URL:-} GENERIC_VIDEO_MODEL: ${GENERIC_VIDEO_MODEL:-} GENERIC_VIDEO_PREDICTION_DEADLINE_SECONDS: ${GENERIC_VIDEO_PREDICTION_DEADLINE_SECONDS:-900} GENERIC_VIDEO_POLL_INTERVAL_MS: ${GENERIC_VIDEO_POLL_INTERVAL_MS:-4000} REPLICATE_API_TOKEN: ${REPLICATE_API_TOKEN:-} REPLICATE_IMAGE_VERSION: ${REPLICATE_IMAGE_VERSION:-} REPLICATE_VIDEO_VERSION: ${REPLICATE_VIDEO_VERSION:-} REPLICATE_LIPSYNC_VERSION: ${REPLICATE_LIPSYNC_VERSION:-} REPLICATE_PREDICTION_DEADLINE_SECONDS: ${REPLICATE_PREDICTION_DEADLINE_SECONDS:-900} STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-} STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET:-} STRIPE_PRO_PRICE_ID: ${STRIPE_PRO_PRICE_ID:-} STRIPE_STUDIO_PRICE_ID: ${STRIPE_STUDIO_PRICE_ID:-} STRIPE_PORTAL_CONFIGURATION_ID: ${STRIPE_PORTAL_CONFIGURATION_ID:-} BILLING_SUCCESS_URL: ${BILLING_SUCCESS_URL:-} BILLING_CANCEL_URL: ${BILLING_CANCEL_URL:-} PUBLISHING_CONNECTOR_URL: ${PUBLISHING_CONNECTOR_URL:-} PUBLISHING_CONNECTOR_SECRET: ${PUBLISHING_CONNECTOR_SECRET:-} PUBLISHING_CONNECTOR_TIMEOUT_MS: ${PUBLISHING_CONNECTOR_TIMEOUT_MS:-30000} FFMPEG_PATH: ffmpeg FFPROBE_PATH: ffprobe WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-2} MAX_UPLOAD_BYTES: ${MAX_UPLOAD_BYTES:-104857600} PROVIDER_DOWNLOAD_TIMEOUT_MS: ${PROVIDER_DOWNLOAD_TIMEOUT_MS:-180000} PROVIDER_DOWNLOAD_MAX_REDIRECTS: ${PROVIDER_DOWNLOAD_MAX_REDIRECTS:-3} services: postgres: image: postgres:16-alpine environment: POSTGRES_DB: ${POSTGRES_DB:-frameflow} POSTGRES_USER: ${POSTGRES_USER:-frameflow} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 5s timeout: 5s retries: 20 restart: unless-stopped redis: image: redis:7.4-alpine environment: REDIS_PASSWORD: ${REDIS_PASSWORD:?Set REDIS_PASSWORD} command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:?Set REDIS_PASSWORD}"] volumes: - redis_data:/data healthcheck: test: ["CMD-SHELL", "redis-cli -a \"$${REDIS_PASSWORD}\" ping | grep -q PONG"] interval: 5s timeout: 5s retries: 20 restart: unless-stopped minio: image: minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: ${S3_ACCESS_KEY:?Set S3_ACCESS_KEY} MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY:?Set S3_SECRET_KEY} MINIO_API_CORS_ALLOW_ORIGIN: ${WEB_ORIGIN:?Set WEB_ORIGIN} MINIO_PROMETHEUS_AUTH_TYPE: public volumes: - minio_data:/data ports: - "${MINIO_CONSOLE_BIND:-127.0.0.1}:${MINIO_CONSOLE_PORT:-9001}:9001" healthcheck: test: ["CMD", "curl", "-f", "http://127.0.0.1:9000/minio/health/live"] interval: 5s timeout: 5s retries: 20 restart: unless-stopped minio-init: image: minio/mc@sha256:a7fe349ef4bd8521fb8497f55c6042871b2ae640607cf99d9bede5e9bdf11727 depends_on: minio: condition: service_healthy environment: S3_BUCKET: ${S3_BUCKET:-frameflow} S3_ACCESS_KEY: ${S3_ACCESS_KEY:?Set S3_ACCESS_KEY} S3_SECRET_KEY: ${S3_SECRET_KEY:?Set S3_SECRET_KEY} entrypoint: ["/bin/sh", "-c"] command: - >- mc alias set local http://minio:9000 "$${S3_ACCESS_KEY}" "$${S3_SECRET_KEY}" && mc mb --ignore-existing "local/$${S3_BUCKET}" && mc anonymous set none "local/$${S3_BUCKET}" restart: "no" config-check: <<: *server-image environment: *server-environment command: ["node", "server/dist/config-check.js"] read_only: true tmpfs: - /tmp:size=16m restart: "no" migrate: <<: *server-image environment: *server-environment command: ["node", "server/dist/db/migrate.js"] depends_on: config-check: condition: service_completed_successfully postgres: condition: service_healthy read_only: true tmpfs: - /tmp:size=128m restart: "no" api: <<: *server-image environment: *server-environment command: ["node", "server/dist/index.js"] depends_on: migrate: condition: service_completed_successfully redis: condition: service_healthy minio-init: condition: service_completed_successfully read_only: true tmpfs: - /tmp:size=512m healthcheck: test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8787/health/ready').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"] interval: 10s timeout: 5s start_period: 15s retries: 10 restart: unless-stopped worker: <<: *server-image environment: *server-environment command: ["node", "server/dist/worker.js"] depends_on: migrate: condition: service_completed_successfully redis: condition: service_healthy minio-init: condition: service_completed_successfully read_only: true tmpfs: - /tmp:size=4g healthcheck: test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:9092/health/ready').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"] interval: 15s timeout: 5s start_period: 15s retries: 10 restart: unless-stopped web: image: frameflow-web:${FRAMEFLOW_VERSION:-latest} build: context: . dockerfile: Dockerfile.web args: VITE_API_URL: /api/v1 environment: APP_HOST: ${APP_HOST:?Set APP_HOST} MEDIA_HOST: ${MEDIA_HOST:?Set MEDIA_HOST} MAX_UPLOAD_SIZE: ${MAX_UPLOAD_SIZE:-100m} ports: - "${HTTP_BIND:-0.0.0.0}:${HTTP_PORT:-8080}:80" depends_on: api: condition: service_healthy minio: condition: service_healthy restart: unless-stopped prometheus: image: prom/prometheus:v3.5.0 command: - --config.file=/etc/prometheus/prometheus.yml - --storage.tsdb.path=/prometheus - --storage.tsdb.retention.time=${PROMETHEUS_RETENTION:-30d} volumes: - ./deploy/prometheus.yml:/etc/prometheus/prometheus.yml:ro - ./deploy/alerts.yml:/etc/prometheus/alerts.yml:ro - prometheus_data:/prometheus ports: - "${PROMETHEUS_BIND:-127.0.0.1}:${PROMETHEUS_PORT:-9090}:9090" depends_on: api: condition: service_healthy worker: condition: service_healthy minio: condition: service_healthy node-exporter: condition: service_healthy alertmanager: condition: service_healthy restart: unless-stopped node-exporter: image: quay.io/prometheus/node-exporter:v1.9.1 command: - --path.rootfs=/host - --collector.textfile.directory=/textfile pid: host volumes: - /:/host:ro - ${FRAMEFLOW_BACKUP_METRICS_DIR:-/var/lib/frameflow/metrics}:/textfile:ro read_only: true cap_drop: - ALL security_opt: - no-new-privileges:true healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:9100/metrics"] interval: 15s timeout: 5s start_period: 5s retries: 10 restart: unless-stopped alertmanager: image: prom/alertmanager:v0.28.1 environment: ALERTMANAGER_WEBHOOK_URL: ${ALERTMANAGER_WEBHOOK_URL:?Set ALERTMANAGER_WEBHOOK_URL} entrypoint: ["/bin/sh", "-ec"] command: - |- case "$${ALERTMANAGER_WEBHOOK_URL}" in https://*) ;; *) echo "ALERTMANAGER_WEBHOOK_URL must use HTTPS" >&2; exit 1 ;; esac escaped_url=$$(printf '%s' "$${ALERTMANAGER_WEBHOOK_URL}" | sed 's/[\\&|]/\\&/g') sed "s|__ALERTMANAGER_WEBHOOK_URL__|$${escaped_url}|g" /etc/alertmanager/alertmanager.yml.template > /tmp/alertmanager.yml /bin/amtool check-config /tmp/alertmanager.yml exec /bin/alertmanager \ --config.file=/tmp/alertmanager.yml \ --storage.path=/alertmanager \ --enable-feature=receiver-name-in-metrics volumes: - ./deploy/alertmanager.yml.template:/etc/alertmanager/alertmanager.yml.template:ro - alertmanager_data:/alertmanager ports: - "${ALERTMANAGER_BIND:-127.0.0.1}:${ALERTMANAGER_PORT:-9093}:9093" read_only: true tmpfs: - /tmp:size=16m healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:9093/-/ready"] interval: 10s timeout: 5s start_period: 5s retries: 10 restart: unless-stopped volumes: postgres_data: redis_data: minio_data: prometheus_data: alertmanager_data: