# HR-ATS-Portal — single Compose file for local and production. # # docker compose up -d --build # # Root `.env` only sets COMPOSE_ENV_FILES=./backend/.env so ${FRONTEND_PORT}, # ${BACKEND_PORT}, ${UVICORN_WORKERS}, … interpolate from backend/.env. # Secrets and app config live solely in backend/.env (loaded into containers # via env_file as well). # # Local (PROD_ENV=false, DB_HOST=localhost in backend/.env): # Containers reach host Postgres via host.docker.internal (db_setup rewrite # when IN_DOCKER=1). Frontend :5173, API :8000 by default. # # Prod (PROD_ENV=true, DB_* = RDS in backend/.env — edit manually): # Same command. No host rewrite; SSL require when DB_SSLMODE is blank. # # Optional Compose Postgres: # docker compose --profile postgres up -d postgres # # Optional live-reload / bind mounts: # docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build # # See DOCKER.md for env checklist and verification. x-logging: &default-logging driver: json-file options: max-size: "10m" max-file: "3" x-backend-build: &backend-build # Root context, not ./backend: backend/job/candidate imports the bulk-ats engine # from app/, which sits outside the backend folder. See backend/Dockerfile. context: . dockerfile: backend/Dockerfile x-backend-env: &backend-env PYTHONPATH: /app # Lets db_setup rewrite DB_HOST=localhost → host.docker.internal (local only). IN_DOCKER: "1" # Credentials and endpoints come from backend/.env via `env_file` below. # DB_HOST is deliberately ABSENT here: an empty `environment:` override would # blank env_file / RDS. Do not name credential keys under environment. # # REDIS_URL and BACKEND_URL are compose-network DNS names, correct everywhere. REDIS_URL: redis://redis:6379/0 BACKEND_URL: http://backend-api:8000 # Apify token, if kept at repo root rather than in backend/.env. Harmless when # unset: the app treats empty as absent. APIFY_API_TOKEN: ${APIFY_API_TOKEN:-${APIFY_TOKEN:-}} # Shared CV storage. Named volume so API + workers see the same files. # Optional docker-compose.dev.yml remounts ./backend/inbox/decoded_attachments. x-attachments: &attachments - attachments-data:/app/inbox/decoded_attachments x-backend-service: &backend-service build: *backend-build image: hrms-backend:local working_dir: /app env_file: # backend/.env is the source of truth (plain DB_* + PROD_ENV). - ./backend/.env # Optional local overrides (required:false). Do not set DB_HOST=postgres # here unless you intentionally start the postgres profile. - path: ./docker.local.env required: false environment: *backend-env extra_hosts: - "host.docker.internal:host-gateway" depends_on: redis: condition: service_healthy # required:false — default stack never starts postgres (host Postgres or RDS). postgres: condition: service_healthy required: false restart: unless-stopped logging: *default-logging services: # --- Redis (broker + result backend for taskiq) ---------------------------------- redis: image: redis:7-alpine container_name: hrms-redis command: ["redis-server", "--appendonly", "yes"] # No host publish by default (Compose DNS redis:6379). Optional loopback: # docker-compose.host-ports.yml volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped logging: *default-logging # --- Postgres: DEFINED HERE, NOT STARTED BY DEFAULT ------------------------------- # Default local path is host Postgres (DB_HOST=localhost → host.docker.internal). # Prod uses AWS RDS. Opt in only when you want a disposable Compose DB: # # docker compose --profile postgres up -d postgres # # then set DB_HOST=postgres in backend/.env and recreate backend services # postgres: profiles: ["postgres"] build: context: ./docker/postgres image: hrms-postgres:local container_name: hrms-postgres environment: # Compose substitution needs: docker compose --env-file ./backend/.env … # (there is no repo-root .env). Defaults apply if the flag is omitted. POSTGRES_USER: ${DB_USERNAME:-postgres} POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} POSTGRES_DB: ${DB_NAME:-hrms} POSTGRES_INITDB_ARGS: "--encoding=UTF8" ports: # 5433: the host's own Postgres owns 5432. - "127.0.0.1:${POSTGRES_PORT:-5433}:5432" volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-postgres} -d ${DB_NAME:-hrms}"] interval: 10s timeout: 5s retries: 5 start_period: 20s restart: unless-stopped logging: *default-logging # --- portal API ------------------------------------------------------------------ backend-api: <<: *backend-service container_name: hrms-backend-api command: [ "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "${UVICORN_WORKERS:-2}", "--proxy-headers", "--forwarded-allow-ips=*", ] # Published for host tools / Vite (`VITE_API_BASE=http://127.0.0.1:8000`). ports: - "${BACKEND_PORT:-8000}:8000" volumes: *attachments healthcheck: test: [ "CMD", "python", "-c", "import urllib.request;urllib.request.urlopen('http://127.0.0.1:8000/health',timeout=3)", ] interval: 15s timeout: 5s retries: 12 # RDS / first-boot Alembic can run well past 40s before /health answers. start_period: 180s # --- bulk ATS scoring engine (standalone service form of app/) -------------------- ats-engine: build: context: . dockerfile: app/Dockerfile image: hrms-ats-engine:local container_name: hrms-ats-engine env_file: - ./backend/.env - path: ./docker.local.env required: false environment: IN_DOCKER: "1" # No host publish by default (backend uses ats-engine:8100). Optional: # docker-compose.host-ports.yml healthcheck: test: [ "CMD", "python", "-c", "import urllib.request;urllib.request.urlopen('http://127.0.0.1:8100/api/v1/health',timeout=3)", ] interval: 15s timeout: 5s retries: 5 start_period: 20s restart: unless-stopped logging: *default-logging # Portal on FRONTEND_PORT (default 5173). API also on BACKEND_PORT (8000). frontend: build: context: ./frontend args: # Empty = same-origin; nginx proxies API paths to backend-api (see nginx.conf). VITE_API_BASE: ${VITE_API_BASE:-} image: hrms-frontend:local container_name: hrms-frontend depends_on: backend-api: condition: service_healthy ports: - "${FRONTEND_PORT:-5173}:80" healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1/"] interval: 15s timeout: 5s retries: 5 restart: unless-stopped logging: *default-logging # --- background processing (same image as backend-api, different command) --------- taskiq-worker: <<: *backend-service container_name: hrms-taskiq-worker command: [ "taskiq", "worker", "taskiq_management.broker_setup:broker", "inbox.tasks", "inbox.sync_tasks", "taskiq_management.tasks", "--workers", "1", ] environment: <<: *backend-env TASKIQ_QUEUE_NAME: inbox TASKIQ_WORKER_NAME: worker-01 volumes: *attachments taskiq-scheduler: <<: *backend-service container_name: hrms-taskiq-scheduler command: [ "taskiq", "scheduler", "taskiq_management.broker_setup:scheduler", "inbox.sync_tasks", ] environment: <<: *backend-env TASKIQ_QUEUE_NAME: inbox taskiq-cv-worker: <<: *backend-service container_name: hrms-taskiq-cv-worker command: [ "taskiq", "worker", "taskiq_management.cv_broker_setup:cv_broker", "inbox.cv_tasks", "--workers", "1", ] environment: <<: *backend-env TASKIQ_CV_QUEUE_NAME: cv_upload TASKIQ_WORKER_NAME: cv-worker-01 volumes: *attachments taskiq-cv-scheduler: <<: *backend-service container_name: hrms-taskiq-cv-scheduler command: [ "taskiq", "scheduler", "taskiq_management.cv_broker_setup:cv_scheduler", "inbox.cv_tasks", ] environment: <<: *backend-env TASKIQ_CV_QUEUE_NAME: cv_upload # Dedicated stream: Google Sheet → FormData import must not block inbox/CV/mailbox. taskiq-sheet-worker: <<: *backend-service container_name: hrms-taskiq-sheet-worker command: [ "taskiq", "worker", "taskiq_management.g_sheet_broker_setup:sheet_broker", "g_sheet.tasks", "--workers", "1", ] environment: <<: *backend-env TASKIQ_SHEET_QUEUE_NAME: sheet_import TASKIQ_WORKER_NAME: sheet-worker-01 # Dedicated stream: Outlook pull/triage must not block match/ATS or CV uploads. taskiq-mailbox-sync-worker: <<: *backend-service container_name: hrms-taskiq-mailbox-sync-worker command: [ "taskiq", "worker", "taskiq_management.mailbox_sync_broker_setup:mailbox_sync_broker", "inbox.mailbox_sync_tasks", "--workers", "1", ] environment: <<: *backend-env TASKIQ_MAILBOX_SYNC_QUEUE_NAME: mailbox_sync TASKIQ_WORKER_NAME: mailbox-sync-worker-01 volumes: *attachments volumes: redis-data: postgres-data: attachments-data: