Files
sciagent/docker-compose.prod.yml
Thinh Lam 688fac73e9
CI/CD / backend (push) Failing after 2m8s
CI/CD / frontend (push) Failing after 1m40s
CI/CD / deploy (push) Has been skipped
sciagent code + Gitea Actions CI/CD
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:38:30 +07:00

212 lines
10 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 Composes default project network; DNS names postgres, be0, minio work.
# Do not set your public VPS IP here — use PUBLIC_HOST + ports in `.env` / `ports:`.