sciagent code + Gitea Actions CI/CD
CI/CD / backend (push) Failing after 2m8s
CI/CD / frontend (push) Failing after 1m40s
CI/CD / deploy (push) Has been skipped

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Thinh Lam
2026-06-30 09:38:30 +07:00
commit 688fac73e9
1167 changed files with 158244 additions and 0 deletions
+211
View File
@@ -0,0 +1,211 @@
# 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:`.