Skip to content

Deployment

_david edited this page Sep 26, 2026 · 2 revisions

Deployment

Pipeline

Two GitHub Actions workflows:

  • Node.js CI (.github/workflows/node.js.yml) — on push/PR to main and staging: npm ci, npm run build --if-present, npm test, on Node 20.x and 22.x. Both build (20.x) and build (22.x) are required status checks on both branches — a PR cannot be merged (by anyone, including the repo owner) until both are green on that exact commit.
  • Build & Deploy to Render (.github/workflows/deploy.yml) — triggered only on push to main: npm install, npm run build, then curls Render's deploy hook URL (RENDER_SRV/RENDER_KEY secrets).

Important: the deploy workflow's "success" only means the deploy hook was triggered (HTTP 2xx from Render's API) — it does not wait for or reflect the result of Render's own build. Render pulls the repo and does its own independent npm install/build after being triggered; that can fail even when the GitHub Actions run shows green. Always verify a deploy by hitting the live URL directly, not by trusting the Actions status alone.

Live URL: https://nodejs-resume-api-ts.onrender.com

Branching model: staging → main, two tiers

This replaced an earlier develop → main model on 2026-08-30. See CONTRIBUTING.md at the repo root for the full rationale; summary:

  • Every feature/fix/hotfix branch is cut from staging, never from main.
  • PRs merge into staging — never straight into main from a feature branch.
  • staging → main only happens through an explicit release step (merge commit, not squash, so git bisect/git blame still resolve to the real originating commit).
  • Both main and staging have real GitHub branch protection: no direct push (enforce_admins: true, so not even the owner can bypass it), and both are gated by the same build (20.x)/build (22.x) required checks described above.
  • required_approving_review_count is deliberately 0 on both — this is a solo-maintainer repo (human + Claude Code both merge PRs), and GitHub refuses to let an account approve its own PR, so requiring ≥1 review would lock the only maintainer out entirely. 0 still forces every change through a reviewable, diffable PR — it just doesn't add a review gate that can't be satisfied.
  • An issue only auto-closes (Closes #N) when the PR merges into the default branch, which is main — a PR merging into staging never auto-closes its issue, even if the body says Closes #N; that's expected, not a bug, and fires for real once the same change reaches main via the release PR.

If using Claude Code: /ship handles feature-branch → staging (commit, push, optionally open+merge the PR); /release handles staging → main (build+test gate, merge commit, version bump, tag, deploy trigger, issue-closing).

Docker

No local Node/Mongo install needed — everything runs in containers, and this is the easiest way to reproduce the exact production topology locally.

Dev (hot reload, insecure built-in dev secrets, no .env required):

docker compose up --build

→ http://localhost:3001/health, includes a MongoDB 7 container with a persistent volume.

Prod (compiled image, real secrets required):

cp .env.example .env   # fill in real TOKEN_SECRET/TOKEN_REFRESH/SESSION_SECRET/CORS_ORIGIN
docker compose -f docker-compose.prod.yml up -d --build

→ http://localhost:3008/health

Chromium is installed via apt in the image (not Puppeteer's own downloader) with PUPPETEER_EXECUTABLE_PATH set — this specifically works around the hardcoded-Chrome-path fragility noted in Security. Redis is intentionally not containerized in either stack — the app already falls back to in-memory when REDIS_URL is unset.

Two real bugs were found and fixed while building/live-testing the Docker setup (both now fixed on main): production always ignoring LOCAL_PORT and hardcoding port 3008 regardless of the env var, and three file-write paths (createPDF.ts, uploadCV.middleware.ts, uploadImages.middleware.ts) being hardcoded as src/public/... instead of resolving relative to __dirname — which 500'd with ENOENT in a minimal production image that only ships dist/, not src/.

Known deploy gotcha: Puppeteer Chrome download cache corruption (non-Docker deploys)

The app depends on the full puppeteer package (not puppeteer-core), which downloads a Chrome binary during npm install. Render persists a build cache (/opt/render/.cache/puppeteer/...) across deploys to speed this up. If a previous build was interrupted mid-download, the cache can end up in a state where the destination folder exists but the actual executable inside is missing — Puppeteer's installer sees the folder, assumes it's already installed, and fails instead of re-downloading:

Error: The browser folder (.../chrome-linux64) exists but the executable (.../chrome) is missing

Fix: on Render's dashboard, use Manual Deploy → "Clear build cache & deploy". This has happened at least once and self-resolved on a subsequent deploy without intervention, but don't assume that will always happen — clear the cache if a deploy fails with this error.

Environment variables on Render

Render's environment variables are configured in its dashboard, separately from this repo's .env.example/.env.development/.env.production files. See Getting Started for the full variable list, including CORS_ORIGIN — Render needs the same set. The real production value of CORS_ORIGIN is a dashboard setting, not tracked in this repo; verify it directly on Render rather than assuming it matches any local .env.

⚠️ If you're developing locally and your .env's MONGO_URI/MONGOBD_USER happens to point at the same database Render uses, your "local" testing is not isolated — double-check which database you're actually pointed at before running destructive tests or scripts (e.g. migrate:localize-text) against it.

Clone this wiki locally