Skip to content

Dockerize the full stack so OpenATS can be self-hosted with a single docker compose up #40

Description

@avikhemka

Problem

The README pitches OpenATS as self-hosted — "teams can run their own hiring pipeline without depending on a SaaS vendor" — but there's no containerized way to actually run it. There are no Dockerfiles in the repo, and backend/docker-compose.yml only starts Postgres and Redis. Every app process is still run by hand on the host.

Standing up an instance today means:

  1. Install Node 22+ and pnpm, pnpm install separately in frontend/ and backend/ (they're independent pnpm projects, each with its own lockfile and pnpm-workspace.yaml)
  2. docker compose up -d from backend/ for Postgres + Redis
  3. Copy and fill two env files
  4. pnpm drizzle-kit generatepnpm drizzle-kit migratepnpm tsx src/db/seed.ts (the seed is required — it inserts the default pipeline stages the app needs to function)
  5. Run three processes in three terminals: frontend, backend API, and the CV-analysis worker

That's a lot of surface area for someone who just wants to evaluate or self-host the thing, and each step is a place to drift from a known-good setup.

The production path has the same gap from the other side. .github/workflows/deploy.yml SSHes into an Azure VM, git pulls, builds on the box, and restarts pm2 — backend only. The frontend has no deploy path in the repo, and ecosystem.config.js (which the workflow restarts) isn't committed, so it exists only on that VM. The running environment isn't reproducible from the repository.

Proposed Solution

A root-level docker-compose.yml that brings the whole system up, plus Dockerfiles for the app processes, so docker compose up yields a working instance.

Services:

  • postgres, redis — promoted from backend/docker-compose.yml
  • api — Express server (pnpm startdist/src/server.js)
  • worker — BullMQ CV-analysis worker (pnpm start:workerdist/src/worker.js). Needs to be its own process, but can share the backend image with a different command
  • web — Next.js frontend
  • migrate — one-shot init container running migrate + seed so a fresh instance works without manual DB steps

Scope / Things to Figure Out

  • Next.js standalone outputfrontend/next.config.ts doesn't set output: "standalone". Without it the runtime image has to carry full node_modules. Adding it is probably right, but it changes the start command and needs the static/public assets copied in.
  • Build-time vs runtime frontend envNEXT_PUBLIC_* and Asgardeo client config get inlined at next build, so the web image is environment-specific unless handled deliberately. Decide between build args (image per deployment) or runtime config (generic image). This is the main design decision in the issue.
  • frontend/.env.example doesn't exist — CONTRIBUTING tells you to cp .env.example .env in frontend/, but that file isn't committed. Compose needs a documented frontend env surface, so adding it belongs to this work.
  • External services can't be containerized away — Asgardeo gates most routes via auth middleware, and R2 / Resend / Gemini / Google Calendar back real features. Compose gets the stack running but auth still needs an Asgardeo app configured per docs/IAM_SETUP.md. Worth stating plainly in the docs so docker compose up doesn't over-promise. Also worth checking which services boot cleanly with the optional keys unset.
  • Port mismatchbackend/.env.example sets PORT=8080, CONTRIBUTING documents the backend on :5000, and the frontend expects one of them. Pin it in compose and reconcile the docs.
  • Build contexts and pinning — two separate contexts, --frozen-lockfile installs, pnpm@11.12.0 to match packageManager, Node 22 alpine to match engines. Multi-stage builds so devDependencies and TS source stay out of the runtime images.
  • Idempotent seed — if migrate runs on every boot, the seed has to be safe to re-run; otherwise make it a documented one-time docker compose run migrate.
  • Healthchecksapi and worker should wait on healthy Postgres and Redis rather than crash-loop on boot ordering.
  • .dockerignore for both apps — keep node_modules, .next, and dist out of build contexts.
  • Socket.IO — the backend serves websockets; if a reverse proxy service is added, upgrades need to keep working.
  • Dev vs. prod split — decide whether to containerize the dev loop too (bind mounts + hot reload via docker-compose.override.yml) or leave dev on the host and target compose at self-hosters. Either way, contributors currently run docker compose up -d from backend/ — that path shouldn't silently break.
  • Relationship to the existing deploy — does compose eventually replace the pm2/Azure workflow, or live alongside it as the self-host story? Probably best to leave deploy.yml untouched in a first PR and treat that as a follow-up.

Publishing images to GHCR would be a natural follow-up, but out of scope here.

Happy to take this on if it sounds useful and the direction above matches what you'd want.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions