services: minio: image: quay.io/minio/minio:latest container_name: minio # Host 9000/9001 are often taken; map to free ports on your machine (S3 API / web UI). ports: - "19000:9000" # API / S3 endpoint → http://localhost:19000 - "19001:9001" # Web console → http://localhost:19001 environment: MINIO_ROOT_USER: minio_user MINIO_ROOT_PASSWORD: minio_password # Use strong password in real projects! # Community MinIO has no per-bucket PutBucketCors (AiStor-only). Browsers need global API CORS for presigned GETs. MINIO_API_CORS_ALLOW_ORIGIN: ${MINIO_API_CORS_ALLOW_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 networks: profyt-net: ipv4_address: "10.5.0.3" # One-shot: ensure buckets. Browser CORS is MINIO_API_CORS_ALLOW_ORIGIN on the minio service (not mc cors set). minio-cors: image: quay.io/minio/mc:latest container_name: minio-cors depends_on: minio: condition: service_healthy entrypoint: ["/bin/sh", "-c"] command: - | mc alias set local http://minio:9000 minio_user minio_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." networks: profyt-net: ipv4_address: "10.5.0.5" restart: "no" postgres: image: postgres:16-alpine container_name: initiative-postgres # Host 5432 is often taken by a local Postgres; map a different port for host access. ports: - "15432:5432" environment: POSTGRES_USER: initiative POSTGRES_PASSWORD: initiative_secret POSTGRES_DB: initiatives volumes: - initiative_pg_data:/var/lib/postgresql/data # Schema lives under be0 (dyd/0backend/migrations is not in this repo). - ./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 healthcheck: test: ["CMD-SHELL", "pg_isready -U initiative -d initiatives"] interval: 3s timeout: 5s retries: 20 start_period: 10s restart: unless-stopped networks: profyt-net: ipv4_address: "10.5.0.10" # ── Frontends ─────────────────────────────────────────────────────────────── # Two SPAs built from the npm workspace (shared kernel + each app). The browser # calls same-origin /api/*; Vite proxies to be0 (localhost:4402 is wrong inside the # container). Build context is the repo ROOT — the workspace — not the app dir, so # `@ump/shared` (../shared/src) resolves. Dev mode: bind-mount the workspace + reinstall # on start so new deps land in the isolated node_modules volume. frontend_user: build: context: . dockerfile: frontend_user/Dockerfile container_name: frontend_user ipc: host ports: - 8081:5173 environment: - GENERIC_TIMEZONE=UTC - VITE_DEV_PROXY_TARGET=http://be0:4402 # When unset, Vite allows all hosts. Set e.g. YOUR_IP,localhost for cloud/LAN dev. - VITE_ALLOWED_HOSTS=${VITE_ALLOWED_HOSTS:-} volumes: - ./package.json:/app/package.json - ./package-lock.json:/app/package-lock.json - ./shared:/app/shared - ./frontend_user:/app/frontend_user - ./frontend_admin:/app/frontend_admin - ./frontend_investigator:/app/frontend_investigator - ./frontend_publisher:/app/frontend_publisher # Isolated workspace-hoisted node_modules (not shadowed by the host). - /app/node_modules command: ["sh", "-c", "npm install && npm run dev -w frontend_user -- --host 0.0.0.0 --port 5173"] depends_on: be0: condition: service_started networks: profyt-net: ipv4_address: "10.5.0.4" frontend_admin: build: context: . dockerfile: frontend_admin/Dockerfile container_name: frontend_admin ipc: host ports: - 8082:5174 environment: - GENERIC_TIMEZONE=UTC - VITE_DEV_PROXY_TARGET=http://be0:4402 - VITE_ALLOWED_HOSTS=${VITE_ALLOWED_HOSTS:-} volumes: - ./package.json:/app/package.json - ./package-lock.json:/app/package-lock.json - ./shared:/app/shared - ./frontend_user:/app/frontend_user - ./frontend_admin:/app/frontend_admin - ./frontend_investigator:/app/frontend_investigator - ./frontend_publisher:/app/frontend_publisher - /app/node_modules command: ["sh", "-c", "npm install && npm run dev -w frontend_admin -- --host 0.0.0.0 --port 5174"] depends_on: be0: condition: service_started networks: profyt-net: ipv4_address: "10.5.0.6" frontend_investigator: build: context: . dockerfile: frontend_investigator/Dockerfile container_name: frontend_investigator ipc: host ports: - 8083:5175 environment: - GENERIC_TIMEZONE=UTC - VITE_DEV_PROXY_TARGET=http://be0:4402 - VITE_ALLOWED_HOSTS=${VITE_ALLOWED_HOSTS:-} volumes: - ./package.json:/app/package.json - ./package-lock.json:/app/package-lock.json - ./shared:/app/shared - ./frontend_user:/app/frontend_user - ./frontend_admin:/app/frontend_admin - ./frontend_investigator:/app/frontend_investigator - ./frontend_publisher:/app/frontend_publisher - /app/node_modules command: ["sh", "-c", "npm install && npm run dev -w frontend_investigator -- --host 0.0.0.0 --port 5175"] depends_on: be0: condition: service_started networks: profyt-net: ipv4_address: "10.5.0.7" frontend_publisher: build: context: . dockerfile: frontend_publisher/Dockerfile container_name: frontend_publisher ipc: host ports: - 8084:5176 environment: - GENERIC_TIMEZONE=UTC - VITE_DEV_PROXY_TARGET=http://be0:4402 - VITE_ALLOWED_HOSTS=${VITE_ALLOWED_HOSTS:-} volumes: - ./package.json:/app/package.json - ./package-lock.json:/app/package-lock.json - ./shared:/app/shared - ./frontend_user:/app/frontend_user - ./frontend_admin:/app/frontend_admin - ./frontend_investigator:/app/frontend_investigator - ./frontend_publisher:/app/frontend_publisher - /app/node_modules command: ["sh", "-c", "npm install && npm run dev -w frontend_publisher -- --host 0.0.0.0 --port 5176"] depends_on: be0: condition: service_started networks: profyt-net: ipv4_address: "10.5.0.8" be0: build: context: ./be0 dockerfile: Dockerfile container_name: be0 ipc: host ports: - 4402:4402 environment: # Dev stack: hot-reload API when bind-mounting ./be0 - UVICORN_RELOAD=1 - GENERIC_TIMEZONE=UTC - INITIATIVE_DATABASE_URL=postgresql+asyncpg://initiative:initiative_secret@postgres:5432/initiatives - APPLICATION_DRAFT_DIR=/app/assets/application-drafts # Shared with fe0 `public/submitted-initiatives` so PDFs written by be0 are served by Vite static. - SUBMITTED_INITIATIVES_DIR=/app/submitted-initiatives # From inside the be0 container, reach MinIO on the shared Docker network (not localhost:19000). - S3_ENDPOINT_URL=http://minio:9000 - S3_ACCESS_KEY=minio_user - S3_SECRET_KEY=minio_password - S3_BUCKET_ATTACHMENTS=initiative-attachments - S3_BUCKET_EXPORTS=initiative-exports - S3_BUCKET_QUARANTINE=initiative-quarantine # Presigned « Xem / tải » links in the browser must hit the host-mapped MinIO port, not minio:9000. - S3_PUBLIC_ENDPOINT_URL=${S3_PUBLIC_ENDPOINT_URL:-http://localhost:19000} # Optional: comma-separated; merged with localhost defaults (e.g. http://YOUR_IP:8081 for LAN deploys). - CORS_ORIGINS=${CORS_ORIGINS:-} # Optional: comma-separated institutional admin emails. When unset, auth_api uses built-in UMP allow-list. - AUTH_ADMIN_EMAILS=${AUTH_ADMIN_EMAILS:-} # SMTP (Option A) — OTP + password-reset email. Set SMTP_HOST (and secrets) in repo-root .env; see .env.example. - 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:-http://localhost:8081} # Dev-only: OTP/link in stdout instead of SMTP — leave unset when using SMTP above. - AUTH_MAIL_LOG_ONLY=${AUTH_MAIL_LOG_ONLY:-} # DOCX mẫu hồ sơ (Xem lại) — cùng file với fe0/public/…/template_application_form.docx - 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 # One-shot minio-cors must finish first so buckets exist (Compose v2.13+). minio-cors: condition: service_completed_successfully networks: profyt-net: ipv4_address: "10.5.0.2" volumes: initiative_pg_data: networks: profyt-net: driver: bridge ipam: config: - subnet: "10.5.0.0/16"