Skip to main content

Environments

The Docker image

A multi-stage build (full detail in Backend):
  • builder: installs all deps, compiles TypeScript → dist/.
  • runner: fresh image, production deps only, copies just dist/, runs as a non-root user.
The final image ships no compiler, no devDeps, no source, so it’s smaller and has a lower attack surface. Both compose files target the runner stage (a mismatch here was a real bug, see Issues).

Local dev with Neon Local

docker-compose.dev.yml runs the app plus a Neon Local proxy that forks a fresh ephemeral Neon branch on up and deletes it on down. scripts/dev.sh (via npm run dev:docker) validates env, waits for the proxy to be healthy, runs migrations, then tails logs.
This project has two compose files and no default docker-compose.yml, so the -f flag is always required. A handy alias: alias dc-dev='docker compose -f docker-compose.dev.yml'.

CI/CD: three GitHub Actions workflows

  1. lint-and-format.yml: runs ESLint and Prettier; reports both failures together (not one-at-a-time) with fix commands as annotations.
  2. tests.yml: spins up a postgres:16-alpine service container, runs migrations, runs npm test, uploads coverage (30-day retention), writes a summary.
  3. docker-build-and-push.yml: on merge to main (or manual), builds linux/amd64 + linux/arm64, tags (branch, SHA, latest, prod-YYYYMMDD-HHmmss), pushes to Docker Hub. Requires DOCKER_USERNAME / DOCKER_PASSWORD repo secrets.

Deploy to Render

Production deploys via a Render Blueprint (render.yaml) using Render’s Docker runtime: Render builds the same Dockerfile you tested locally.
Migrations are not automatic: Render runs the app, not migrations. Run once against the prod DB before/after first deploy:

The flow, end to end

Frontend CI/CD (sportz-ui)

The frontend has its own pipeline, mirroring the backend’s conventions. CI: two GitHub Actions workflows:
  1. lint-and-format.yml: ESLint + Prettier, both reported together with a gate (same pattern as the backend). Node 22 (matches the frontend’s Dockerfile and Next 16).
  2. tests.yml: two parallel jobs: Typecheck (tsc --noEmit) and E2E (Playwright) (builds a production server on :3100 and runs the mocked suite + axe a11y scan). No database/service container: the e2e suite mocks REST/WS at the network boundary.
CD: Vercel (Git integration): Vercel builds and deploys on every push to main, with a preview deployment per PR. This is the CD; there’s no separate deploy workflow. NEXT_PUBLIC_* env vars are set in Vercel’s project settings (they’re baked at build time, so changing one requires a redeploy). CORS_ORIGIN on the Render backend must include the Vercel origin or the live site is blocked (ADR-009). Branch protection connects CI to CD. main requires the three checks (Typecheck, E2E (Playwright), Lint and Format Check) to pass and a pull request before merge, with admin bypass disabled. Since Vercel’s production deploy comes from main, gating main effectively gates production, even though Vercel itself doesn’t read CI. PR preview deploys still happen regardless (you want to preview WIP).
Dockerized too (optional). The frontend has a standalone Dockerfile (gated behind DOCKER_BUILD=1 so next start and Vercel are unaffected) for own-infra/learning use, but Vercel is the live deploy. Vercel fits a client-rendered app better (managed CDN, image optimization, preview URLs) than self-hosting a container would (ADR-009).

Not yet built

  • Rollback automation: Render keeps prior deploys (manual rollback in dashboard); no scripted rollback.
  • Graceful shutdown on deploy: SIGTERM currently drops WS connections uncleanly (close() exists as the building block; see Backend).
  • Automated post-deploy smoke: the deployed smoke test (Testing) is run manually (npm run test:e2e:deployed); a workflow that runs it automatically after a Vercel deploy is a logical next step.
  • Pre-commit enforcement: lint/format are enforced today by the npm scripts (local, manual) and by CI as the merge gate. A husky + lint-staged pre-commit hook would auto-run eslint --fix + prettier --write on staged files so unformatted code can’t even be committed, and commitlint would enforce a commit-message convention (Conventional Commits). Planned tooling, not yet wired.