212 lines
10 KiB
YAML
212 lines
10 KiB
YAML
# 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:`.
|