# Requires a `.env` next to this file (or exported vars). # Validates: scripts/verify-prod-env.sh # # Images are pinned instead of `:latest` for reproducible builds and supply-chain hygiene. services: minio: image: quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z container_name: minio ports: - "${MINIO_API_PORT}:9000" # S3 API → http://${PUBLIC_HOST}:${MINIO_API_PORT} - "127.0.0.1:${MINIO_CONSOLE_PORT}:9001" # Console admin-only via SSH tunnel environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} # Public URL browsers use for the S3 API (match reverse-proxy TLS scheme/host when applicable). MINIO_SERVER_URL: ${MINIO_SERVER_URL:-http://${PUBLIC_HOST}:${MINIO_API_PORT}} MINIO_BROWSER_REDIRECT_URL: ${MINIO_BROWSER_REDIRECT_URL:-http://${PUBLIC_HOST}:${MINIO_CONSOLE_PORT}} # Community MinIO has no per-bucket PutBucketCors; set explicit SPA origin(s) in `.env`. MINIO_API_CORS_ALLOW_ORIGIN: ${MINIO_API_CORS_ALLOW_ORIGIN:?Set MINIO_API_CORS_ALLOW_ORIGIN to your HTTPS SPA origin} volumes: - ./assets/minio-data:/data command: server /data --console-address ":9001" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s timeout: 20s retries: 3 restart: unless-stopped # One-shot: ensure buckets. Browser CORS is MINIO_API_CORS_ALLOW_ORIGIN on the minio service. minio-cors: image: quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z container_name: minio-cors depends_on: minio: condition: service_healthy environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} entrypoint: ["/bin/sh", "-c"] command: - | mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD" for b in initiative-attachments initiative-exports initiative-quarantine imagehub-blobs; do mc mb -p "local/$$b" 2>/dev/null || true done echo "MinIO buckets ensured." # Auth + roles: POSTGRES_* apply only on first volume init — see docs/deploy-production-docker.md postgres: image: postgres:16-alpine container_name: initiative-postgres # Bind to localhost only — DB is not for the public internet. ports: - "127.0.0.1:15432:5432" environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - initiative_pg_data:/var/lib/postgresql/data - ./be0/migrations/001_initiative_schema.sql:/docker-entrypoint-initdb.d/01_initiative_schema.sql:ro - ./be0/migrations/002_application_storage_extensions.sql:/docker-entrypoint-initdb.d/02_application_storage_extensions.sql:ro - ./be0/migrations/003_review_documents.sql:/docker-entrypoint-initdb.d/03_review_documents.sql:ro - ./be0/migrations/004_evidence_artifact_review.sql:/docker-entrypoint-initdb.d/04_evidence_artifact_review.sql:ro - ./be0/migrations/004_application_admin_results.sql:/docker-entrypoint-initdb.d/05_application_admin_results.sql:ro - ./be0/migrations/006_user_notifications.sql:/docker-entrypoint-initdb.d/06_user_notifications.sql:ro - ./be0/migrations/007_user_roles_email_policy_admin.sql:/docker-entrypoint-initdb.d/07_user_roles_email_policy_admin.sql:ro - ./be0/migrations/008_audit_events.sql:/docker-entrypoint-initdb.d/08_audit_events.sql:ro - ./be0/migrations/009_backup_artifact_roles_storage_kind.sql:/docker-entrypoint-initdb.d/09_backup_artifact_roles_storage_kind.sql:ro - ./be0/migrations/010_user_staff_profiles.sql:/docker-entrypoint-initdb.d/10_user_staff_profiles.sql:ro - ./be0/migrations/011_academic_titles_vn.sql:/docker-entrypoint-initdb.d/11_academic_titles_vn.sql:ro - ./be0/migrations/012_password_reset.sql:/docker-entrypoint-initdb.d/12_password_reset.sql:ro - ./be0/migrations/013_email_verification.sql:/docker-entrypoint-initdb.d/13_email_verification.sql:ro - ./be0/migrations/014_registration_otp.sql:/docker-entrypoint-initdb.d/14_registration_otp.sql:ro - ./be0/migrations/015_document_templates.sql:/docker-entrypoint-initdb.d/15_document_templates.sql:ro - ./be0/migrations/016_research_projects.sql:/docker-entrypoint-initdb.d/16_research_projects.sql:ro - ./be0/migrations/017_imagehub_datasets.sql:/docker-entrypoint-initdb.d/17_imagehub_datasets.sql:ro - ./be0/migrations/018_imagehub_segmentation_links.sql:/docker-entrypoint-initdb.d/18_imagehub_segmentation_links.sql:ro - ./be0/migrations/019_imagehub_cloud_import.sql:/docker-entrypoint-initdb.d/19_imagehub_cloud_import.sql:ro - ./be0/migrations/020_imagehub_dataset_stages.sql:/docker-entrypoint-initdb.d/20_imagehub_dataset_stages.sql:ro - ./be0/migrations/021_imagehub_task_pipeline.sql:/docker-entrypoint-initdb.d/21_imagehub_task_pipeline.sql:ro - ./be0/migrations/022_imagehub_task_annotations.sql:/docker-entrypoint-initdb.d/22_imagehub_task_annotations.sql:ro - ./be0/migrations/023_imagehub_dataset_members.sql:/docker-entrypoint-initdb.d/23_imagehub_dataset_members.sql:ro - ./be0/migrations/024_imagehub_dataset_project_link.sql:/docker-entrypoint-initdb.d/24_imagehub_dataset_project_link.sql:ro - ./be0/migrations/025_imagehub_task_review_events.sql:/docker-entrypoint-initdb.d/25_imagehub_task_review_events.sql:ro - ./be0/migrations/026_imagehub_file_folder_path.sql:/docker-entrypoint-initdb.d/26_imagehub_file_folder_path.sql:ro - ./be0/migrations/027_imagehub_dataset_label_map.sql:/docker-entrypoint-initdb.d/27_imagehub_dataset_label_map.sql:ro # Evaluate user/db inside the container ($$…) so Compose .env substitution stays in sync at runtime. healthcheck: test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""] interval: 10s timeout: 5s retries: 12 start_period: 30s restart: unless-stopped # API — must become healthy (Postgres + MinIO + successful startup) before fe0 starts. be0: build: context: ./be0 dockerfile: Dockerfile container_name: be0 ipc: host ports: - "127.0.0.1:4402:4402" healthcheck: test: ["CMD-SHELL", "curl -sf http://127.0.0.1:4402/health >/dev/null"] interval: 10s timeout: 10s retries: 15 start_period: 180s environment: - GENERIC_TIMEZONE=UTC - ENVIRONMENT=production - JWT_SECRET=${JWT_SECRET:?Set JWT_SECRET in .env — openssl rand -base64 48} - INITIATIVE_DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} - APPLICATION_DRAFT_DIR=/app/assets/application-drafts - SUBMITTED_INITIATIVES_DIR=/app/submitted-initiatives - S3_ENDPOINT_URL=http://minio:9000 - S3_ACCESS_KEY=${MINIO_ROOT_USER} - S3_SECRET_KEY=${MINIO_ROOT_PASSWORD} - S3_BUCKET_ATTACHMENTS=initiative-attachments - S3_BUCKET_EXPORTS=initiative-exports - S3_BUCKET_QUARANTINE=initiative-quarantine # Presigned GET/PUT host the browser opens — must be HTTPS when the SPA is HTTPS (see docs/minio-behind-https.md). - S3_PUBLIC_ENDPOINT_URL=${S3_PUBLIC_ENDPOINT_URL:-http://${PUBLIC_HOST}:${MINIO_API_PORT}} - CORS_ORIGINS=http://${PUBLIC_HOST}:${FE_PORT},${CORS_ORIGINS_EXTRA:-} - AUTH_ADMIN_EMAILS=${AUTH_ADMIN_EMAILS:-} # SMTP — registration OTP + password reset (same vars as docs; set in `.env`). - SMTP_HOST=${SMTP_HOST:-} - SMTP_PORT=${SMTP_PORT:-587} - SMTP_USER=${SMTP_USER:-} - SMTP_PASSWORD=${SMTP_PASSWORD:-} - AUTH_MAIL_FROM=${AUTH_MAIL_FROM:-} - SMTP_USE_TLS=${SMTP_USE_TLS:-1} - AUTH_PUBLIC_WEB_ORIGIN=${AUTH_PUBLIC_WEB_ORIGIN:-} - AUTH_MAIL_LOG_ONLY=${AUTH_MAIL_LOG_ONLY:-} - TEMPLATE_APPLICATION_FORM_DOCX=/app/template_application_form.docx volumes: - ./be0:/app - ./assets:/app/assets - ./assets/submitted-initiatives:/app/submitted-initiatives - ./fe0/public/assets/template_application_form.docx:/app/template_application_form.docx:ro depends_on: postgres: condition: service_healthy minio: condition: service_healthy # Dockerfile entrypoint: NLTK download + pip install + uvicorn (no --reload in prod). restart: unless-stopped # Public applicant SPA — minified static build served by nginx (NOT the Vite dev server). # Build context is the repo root (npm workspace); see frontend_user/Dockerfile.prod. frontend_user: build: context: . dockerfile: frontend_user/Dockerfile.prod container_name: frontend_user ports: - "${FE_PORT}:8080" depends_on: be0: condition: service_healthy restart: unless-stopped # Admin / council SPA — also a hardened static build, but bound to LOCALHOST only. # Reach it via an SSH tunnel or a separate authenticated reverse-proxy vhost. # NOTE: the council review UI is still in progress — keep it off the public internet for now. frontend_admin: build: context: . dockerfile: frontend_admin/Dockerfile.prod container_name: frontend_admin ports: - "127.0.0.1:${FE_ADMIN_PORT:-8082}:8080" depends_on: be0: condition: service_healthy restart: unless-stopped # Principal-investigator SPA (research proposals + project cockpit) — hardened static build. frontend_investigator: build: context: . dockerfile: frontend_investigator/Dockerfile.prod container_name: frontend_investigator ports: - "${FE_INV_PORT:-8083}:8080" depends_on: be0: condition: service_healthy restart: unless-stopped frontend_publisher: build: context: . dockerfile: frontend_publisher/Dockerfile.prod container_name: frontend_publisher ports: - "${FE_PUB_PORT:-8084}:8080" depends_on: be0: condition: service_healthy restart: unless-stopped volumes: initiative_pg_data: # All services join Compose’s default project network; DNS names postgres, be0, minio work. # Do not set your public VPS IP here — use PUBLIC_HOST + ports in `.env` / `ports:`.