The standalone build
apps/saas/next.config.ts:
output: "standalone"makesnext buildemitapps/saas/.next/standalone/, a self contained tree withapps/saas/server.js, the traced workspace packages and a slimnode_modules.outputFileTracingRootpoints at the monorepo root so files frompackages/*are traced in.deploymentIdties the client bundle to a release, which lets Next.js detect a browser running an older build.
apps/saas/public holds the trainee app preview bundle, the assistant avatar images and the wizard assets. If a file is not on disk under public when the release is packaged, it is not in production.
The server is started with node apps/saas/server.js. It reads PORT and HOSTNAME.
Build time and run time variables
The
Dockerfile and both workflows still pass DATABASE_URL and BETTER_AUTH_SECRET into the build. No code in apps or packages reads either. In dev.yml, BETTER_AUTH_SECRET is only used as the input for deriving the server actions encryption key. The Dockerfile supplies obvious placeholders for both.SERVICE_AUTH_SECRET must match the core API’s value, or every signed call fails.
The Docker image
frontend/Dockerfile is a two stage build on node:22-bookworm-slim.
Builder:
corepack enable, copy the repo,pnpm install --frozen-lockfile.- Take
NEXT_PUBLIC_SAAS_URL,NEXT_PUBLIC_SITE_URLandCORE_API_URLas build arguments. pnpm build(both apps through Turborepo).pnpm --filter=marketing build, then copy the export intoapps/saas/public/mktandmkt-assets.
- Copy
.next/standalone,.next/staticandpublic. - Set
NODE_ENV=production,PORT=3043and aHOSTNAMEthat binds all interfaces inside the container. CMD ["node", "apps/saas/server.js"].
.dockerignore excludes node_modules, .next, .turbo, .git, .github, env files and logs.
Workflows
Three files in.github/workflows.
deploy-do.yml passes the public production URLs as build arguments: the app at https://app.byperform.co.il and the API at https://perform-api.otherwise.co.il. It uses a concurrency group per ref, so a newer push cancels an in flight deploy.
So a push to dev deploys through Docker. main has no deploy workflow in this repo.
The PM2 flow (dev.yml)
Kept for manual use. In order:
- Install, derive
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY(a SHA-256 of a fixed prefix plusBETTER_AUTH_SECRET, masked in the log), type-check, build. - Package: standalone output, static files,
public, the marketing export, thedeploy/scripts andecosystem.config.cjs, into one tarball named with a UTC timestamp. - Upload with a checksum comparison on both ends and up to three attempts. A truncated upload can otherwise exit zero and only fail later at extraction.
- On the server: extract into
releases/<id>/, rundeploy/dev/release.sh <id>.
deploy/dev/ scripts:
ecosystem.config.cjs defines one PM2 app, byperform-app, running apps/saas/server.js in fork mode with one instance, --env-file=.env, a 1 GB memory restart limit and PORT 3043.
No database step
Neither workflow touches a database. The web app has no schema of its own any more. Auth and billing tables are owned by the core API.Local production run
dev.sh loads .env and starts node apps/saas/server.js on port 3010. Copy .next/static and public into the standalone tree first if you need assets to load, as the release packaging does.
Checklist before a release
pnpm type-check,pnpm lintandpnpm testpass.- Translations for new keys exist in
enandhe. - If the mobile app changed, the trainee preview bundle under
apps/saas/public/trainee-previewwas re-exported. See Trainee app previews. - If the oRPC router changed in the backend,
orpc-router.generated.d.tsmatches it. - If the plan catalog changed in the backend,
packages/payments/config.tsmatches it. - The core API release that the web build expects is already deployed. The web app is useless without a compatible API.