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:
- 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)
docker compose up -d from backend/ for Postgres + Redis
- Copy and fill two env files
pnpm drizzle-kit generate → pnpm drizzle-kit migrate → pnpm tsx src/db/seed.ts (the seed is required — it inserts the default pipeline stages the app needs to function)
- 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 start → dist/src/server.js)
worker — BullMQ CV-analysis worker (pnpm start:worker → dist/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 output —
frontend/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 env —
NEXT_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 mismatch —
backend/.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.
- Healthchecks —
api 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.
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.ymlonly starts Postgres and Redis. Every app process is still run by hand on the host.Standing up an instance today means:
pnpm installseparately infrontend/andbackend/(they're independent pnpm projects, each with its own lockfile andpnpm-workspace.yaml)docker compose up -dfrombackend/for Postgres + Redispnpm drizzle-kit generate→pnpm drizzle-kit migrate→pnpm tsx src/db/seed.ts(the seed is required — it inserts the default pipeline stages the app needs to function)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.ymlSSHes into an Azure VM,git pulls, builds on the box, and restarts pm2 — backend only. The frontend has no deploy path in the repo, andecosystem.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.ymlthat brings the whole system up, plus Dockerfiles for the app processes, sodocker compose upyields a working instance.Services:
postgres,redis— promoted frombackend/docker-compose.ymlapi— Express server (pnpm start→dist/src/server.js)worker— BullMQ CV-analysis worker (pnpm start:worker→dist/src/worker.js). Needs to be its own process, but can share the backend image with a different commandweb— Next.js frontendmigrate— one-shot init container running migrate + seed so a fresh instance works without manual DB stepsScope / Things to Figure Out
frontend/next.config.tsdoesn't setoutput: "standalone". Without it the runtime image has to carry fullnode_modules. Adding it is probably right, but it changes the start command and needs the static/public assets copied in.NEXT_PUBLIC_*and Asgardeo client config get inlined atnext 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.exampledoesn't exist — CONTRIBUTING tells you tocp .env.example .envinfrontend/, but that file isn't committed. Compose needs a documented frontend env surface, so adding it belongs to this work.docker compose updoesn't over-promise. Also worth checking which services boot cleanly with the optional keys unset.backend/.env.examplesetsPORT=8080, CONTRIBUTING documents the backend on:5000, and the frontend expects one of them. Pin it in compose and reconcile the docs.--frozen-lockfileinstalls,pnpm@11.12.0to matchpackageManager, Node 22 alpine to matchengines. Multi-stage builds sodevDependenciesand TS source stay out of the runtime images.migrateruns on every boot, the seed has to be safe to re-run; otherwise make it a documented one-timedocker compose run migrate.apiandworkershould wait on healthy Postgres and Redis rather than crash-loop on boot ordering..dockerignorefor both apps — keepnode_modules,.next, anddistout of build contexts.docker-compose.override.yml) or leave dev on the host and target compose at self-hosters. Either way, contributors currently rundocker compose up -dfrombackend/— that path shouldn't silently break.deploy.ymluntouched 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.