# HR-ATS-Portal — every service and every image, in one file. # # docker compose build # hrms-backend / hrms-ats-engine / hrms-frontend # docker compose up -d # docker compose ps # docker compose logs -f backend-api # # ── Postgres ────────────────────────────────────────────────────────────────────── # The `postgres` service at the bottom is behind a compose PROFILE, so it is defined # and buildable here but never starts with a plain `docker compose up`. The stack # talks to the PostgreSQL server already running on the host, via # DB_HOST=host.docker.internal (backend/.env says localhost — correct for a host # process, wrong inside a container). # # Host Postgres must accept connections from the Docker bridge: listen_addresses = '*' # in postgresql.conf and a pg_hba.conf line for 172.16.0.0/12 (or the specific subnet). # # To build/run the containerised database instead, see the comments on that service. # # ── Ports ───────────────────────────────────────────────────────────────────────── # Stop a host `uvicorn` (8000) and `npm run dev` (5173) before starting these, or # override with BACKEND_PORT / FRONTEND_PORT / ATS_PORT. Windows lets a host process # bind 127.0.0.1:8000 while Docker binds 0.0.0.0:8000, and `localhost` resolves to ::1 # first — both listen, and requests reach whichever won. # # ── Overlay ─────────────────────────────────────────────────────────────────────── # docker-compose.dev.yml adds live source mounts and --reload on top of this file. It # defines no services or images of its own; it only overrides the ones here: # docker compose -f docker-compose.yml -f docker-compose.dev.yml up 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 # backend/.env is written for host processes; these are the values a container needs. # # DB_HOST defaults to the LOCAL Postgres server on the host. Set DB_HOST=postgres in # the shell (or a root .env) to point the whole stack at the container below instead # — that is the only value that has to change, since services reach it over the # compose network on 5432, not the published host port. DB_HOST: ${DB_HOST:-host.docker.internal} REDIS_URL: redis://redis:6379/0 EMAIL_URL: http://host.docker.internal:5000 BACKEND_URL: http://backend-api:8000 # The one shared folder. Every process that decodes, scores or serves a CV reads and # writes the same host directory, so a file written by the API is the same file the # worker opens. Absolute paths stored in the DB match across services because the # mount target is identical everywhere; inbox.plugins.resolve_attachment_path also # falls back to basename-under-this-directory for rows written by a host process. x-attachments: &attachments - ${ATTACHMENTS_DIR:-./backend/inbox/decoded_attachments}:/app/inbox/decoded_attachments x-backend-service: &backend-service build: *backend-build image: hrms-backend:local working_dir: /app env_file: - ./backend/.env environment: *backend-env extra_hosts: - "host.docker.internal:host-gateway" depends_on: redis: condition: service_healthy # required:false — the default stack uses the host's Postgres and never starts # this one, and that must not be an error. When the profile IS active, startup # waits for it to pass pg_isready. postgres: condition: service_healthy required: false restart: unless-stopped services: # --- Redis (broker + result backend for taskiq) ---------------------------------- redis: image: redis:7-alpine container_name: hrms-redis command: ["redis-server", "--appendonly", "yes"] ports: - "${REDIS_PORT:-6379}:6379" volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped # --- portal API ------------------------------------------------------------------ backend-api: <<: *backend-service container_name: hrms-backend-api command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] ports: - "${BACKEND_PORT:-8000}:8000" volumes: *attachments healthcheck: # python:slim has no curl and the API exposes no /health route, so this is a # plain TCP check against the uvicorn socket. test: [ "CMD", "python", "-c", "import socket;socket.create_connection(('127.0.0.1',8000),3).close()", ] interval: 15s timeout: 5s retries: 5 start_period: 40s # --- 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: # OPENAI_API_KEY currently lives in backend/.env; a root .env (see .env.example) # overrides it when present, and the stack still comes up when it is not. - ./backend/.env - path: ./.env required: false ports: - "${ATS_PORT:-8100}:8100" 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 # --- React portal ----------------------------------------------------------------- frontend: build: context: ./frontend args: # Baked into the bundle at build time — change it and rebuild, not restart. VITE_API_BASE: ${VITE_API_BASE:-http://localhost:8000} image: hrms-frontend:local container_name: hrms-frontend 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 # --- 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 # --- Postgres: DEFINED HERE, NOT STARTED BY DEFAULT ------------------------------- # The profile is what keeps it out of `docker compose build` and `docker compose up`. # Nothing about the default stack changes by its presence in this file. # # docker compose --profile postgres build postgres # build the image # docker compose --profile postgres up -d postgres # run it, host port 5433 # # Pointing the app at it is a separate, deliberate step — set DB_HOST=postgres (see # x-backend-env) and recreate the services. The volume starts empty, so Alembic # rebuilds the schema on first boot; it does not share the host server's data. postgres: profiles: ["postgres"] build: context: ./docker/postgres image: hrms-postgres:local container_name: hrms-postgres environment: # Interpolated from the shell or a root .env, NOT from backend/.env — compose # variable substitution and container environment are different things. # Defaults match backend/.env.example. 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 server owns 5432. Only for host-side tools — # containers reach this one on 5432 over the compose network. - "${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 volumes: redis-data: postgres-data: