Deployment Best Practices
Concrete rules for an application service to run correctly on Wakastart: .env.example, Dockerfile, Prisma migrations, data seeding, runtime config, S3, OAuth.
Deployment Best Practices
These rules come from incidents encountered on real services. They cover everything that can silently break a deployment: missing config, poorly built Docker image, forgotten DB migrations, misaligned Next.js runtime config, shaky OAuth/Keycloak integration.
1. Environment variables
Each .env.example file constitutes a configuration contract. It must be exhaustive, self-documented, and kept up to date with every evolution of the project.
1.1 Mandatory rules for each variable
| Icon | Attribute | Requirement |
|---|---|---|
| 📝 | Example value | Provide a real, functional example value. No empty placeholder like xxx or changeme. |
| 📌 | Contextual description | Explain in one sentence why this variable is needed for the application to work. |
| 🔁 | Cross-service duplicate | If the same value is expected in another microservice (e.g. front AND back), document the duplication, explain why it exists, and state that any change must be propagated to all affected services. |
| ✅ / ⚠️ | Required or optional | Clearly state whether the variable is required or optional, and describe the impact of omitting it (degraded behavior, fatal error, disabled feature…). |
| 🌐 | URL type | If the value is a URL, specify whether it must be public (accessible from the browser) or private (accessible only from the internal network / K8s pod). |
| 🧪 | Dev only | If the variable is only used in dev, state this explicitly and explain why it isn't needed in prod. |
| 🔐 | Secret | If reading this value by an unauthorized person poses a risk (data leak, dangerous access — e.g. DATABASE_URL, API keys, tokens), mark it as SECRET. |
| 🔑 | Encryption / authorization impact | If the variable affects an encryption scheme or an authorization system, document: (1) the services that must share the same value, (2) the risks of changing it (e.g. permanent loss of access to encrypted data), (3) the expected format and the generation command. |
1.2 Documentation example
env# URL of the backend API consumed by the frontend. # TYPE: public URL (accessible from the user's browser). # REQUIRED — without this variable, all API requests fail. # DUPLICATE: This variable corresponds to BACKEND_URL on the backend side. # Changing it here requires updating BACKEND_URL in the backend service. NEXT_PUBLIC_API_URL=https://api.exemple.com # Encryption key for session tokens. # TYPE: SECRET — never commit in plain text nor change without a migration plan. # WARNING: Changing this value invalidates all active tokens. # FORMAT: 32 bytes hex — generate with: openssl rand -hex 32 ENCRYPTION_KEY=<generate with openssl rand -hex 32>
1.3 NEXT_PUBLIC_* vs runtime variables (Next.js)
NEXT_PUBLIC_* variables are compiled at build time by Next.js and inlined into the JS bundle. They cannot be changed at container runtime.
If a URL needs to be configurable without a rebuild (e.g. Keycloak URL, API URL), it must go through the window.__RUNTIME_CONFIG__ mechanism injected by docker-entrypoint.sh (see section 4).
Explicitly document in each service's .env.example:
- which variables are compiled at build time (
NEXT_PUBLIC_*) - which ones are injected at runtime via
window.__RUNTIME_CONFIG__ - in case of overlap between the two mechanisms, which one takes priority and why
⚠️ Never commit a real
.envfile to the Git repository. Only the.env.example(with no real secret values) is versioned.
2. Dockerfile and Prisma migrations
In the absence of advanced CI/CD tooling, any application requiring a database must guarantee that Prisma migrations are applied automatically when the container starts.
2.1 Mandatory entrypoint for applications using Prisma
The Dockerfile of any backend application using Prisma must delegate startup to an entrypoint.sh script. This script applies migrations before starting the main Node.js process.
Typical structure of an entrypoint.sh:
sh#!/bin/sh set -e echo '[entrypoint] Running Prisma migrations...' npx prisma migrate deploy echo '[entrypoint] Starting application...' exec node dist/main.js
Associated Dockerfile configuration:
dockerfileFROM node:20-alpine WORKDIR /app COPY . . RUN npm install --omit=dev RUN npx prisma generate RUN npm run build COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"]
2.2 Pitfalls to avoid
- ✅
prismamust be listed independencies(and notdevDependencies) to be available when the production container runs. - 🚨 Never use
pnpm prune --prodwithout having verified thatprisma,prisma generate, and all required native binaries (bcrypt,multer…) are in the productiondependencies.
2.3 Optional seed: injecting initial or test data
Your service's container is the only entry point to its live database. To re-inject data (reference dataset, demo data, or your local environment's test data) into a Wakastart deployment, the right place is the same entrypoint.sh, after migrations are applied and before the application starts.
The seed must be controllable via an environment variable: a container restarts regularly (rollout, scale, crash), so you don't want it to run again on every startup. The variable lets you enable it temporarily on the desired environment (typically dev), then disable it once the data has been injected.
Step 1 — Declare the seed command in package.json:
json{ "prisma": { "seed": "ts-node prisma/seed.ts" } }
npx prisma db seedruns the command defined in theprisma.seedblock. You can also use your own script (npm run seed).
Step 2 — Condition the seed in entrypoint.sh:
sh#!/bin/sh set -e echo '[entrypoint] Running Prisma migrations...' npx prisma migrate deploy # Optional seed — only if RUN_SEED=true if [ "$RUN_SEED" = "true" ]; then echo '[entrypoint] RUN_SEED=true → seeding database...' npx prisma db seed else echo '[entrypoint] RUN_SEED not enabled → skipping seed.' fi echo '[entrypoint] Starting application...' exec node dist/main.js
Step 3 — Control RUN_SEED via Wakastart variables: the variable is defined like any other config var of your service (see Variables & Secrets). Set RUN_SEED=true on the environment where you want to inject the data (e.g. a dev env), deploy, then set the variable back to false once the seed has been applied, to avoid it running again on the next restart.
Best practices:
- ✅ The seed script must be idempotent: use
upsert(or a guard "do nothing if the table is already populated") rather thancreate, so it can be replayed without creating duplicates. - ✅
ts-node(or whatever tool your seed command uses) must be available at runtime in the image — place it independenciesif you call it at runtime, or compile the seed script along with the rest of the application.
3. TypeScript and NestJS / Node.js build
TypeScript build errors are a frequent cause of silently broken deployments.
| Rule | Detail |
|---|---|
rootDir required in tsconfig.build.json | Always declare rootDir: "./src" to guarantee that the compiled output lands in dist/main.js and not dist/src/main.js. Without rootDir, the output path depends on the resolved module and breaks the container's startup. |
Avoid imports outside rootDir | Never import from ../package.json or any path located outside the defined rootDir. Such imports cause tsc errors and are not accessible inside the production Docker image. |
postinstall scripts with pnpm v10+ | pnpm v10 blocks postinstall scripts by default. Explicitly configure pnpm.onlyBuiltDependencies in package.json to allow the generation of Prisma, bcrypt, and other native dependency binaries. |
| Native dependencies in production | multer, bcrypt, and any package required at runtime must be in dependencies, not devDependencies, even when used through a framework like NestJS. |
prisma.config.ts without dotenv | Do not import dotenv/config in prisma.config.ts. dotenv is not available in the production image, and environment variables are injected by the orchestration system (K8s). |
4. Frontend runtime config (Next.js)
Next.js applications deployed as containers have two configuration sources: NEXT_PUBLIC_* variables (compiled at build time) and variables injected at runtime via window.__RUNTIME_CONFIG__. Variable names must be strictly aligned between docker-entrypoint.sh and the Kubernetes deployment.
Rules for runtime config
- The frontend's
entrypoint.shmust read variables exactly as defined in the K8s deployment (NEXT_PUBLIC_API_URL,KEYCLOAK_PUBLIC_URL, etc.), and not locally invented aliases (API_URL,KEYCLOAK_URL…). - Any URL that might change depending on the environment without a rebuild of the image (e.g. Keycloak URL) must be injected via
window.__RUNTIME_CONFIG__and not read viaprocess.env.NEXT_PUBLIC_*. - Document in each service's
.env.examplewhich variables are compiled at build time (NEXT_PUBLIC_*) and which are injected at runtime. - In case of overlap between a
NEXT_PUBLIC_*variable and a runtime variable, explicitly document which one takes priority and why.
⚠️ An
entrypoint.shthat reads nonexistent variables leaveswindow.__RUNTIME_CONFIG__with hardcoded default values (localhost,preprod…), which is undetectable without logs.
5. S3 and Object Storage integration
S3 adapters must correctly handle endpoint URLs to avoid silent connectivity errors.
Rules for S3 endpoints
- Never mechanically prefix
https://onto an endpoint variable: if the variable already contains the protocol, double-prefixing produces an invalid URL (https://https://…) that linters cannot detect. - The adapter must detect whether the endpoint already contains a protocol, parse the URL to derive the port (
443forhttps,80forhttp), and not force the default port9000(which is the local MinIO port, not the OVH Object Storage port). - Document in
.env.examplewhether theMINIO_ENDPOINT/S3_ENDPOINTvariable should or should not include the protocol, the port, and the path. - Prefer standard S3-style variables such as:
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY, etc.
6. Authentication and OAuth / Keycloak flow
See also Authentication — Concepts & Flow for the general OAuth flow. The rules below are concrete pitfalls encountered on production services.
6.1 redirect_uri
The redirect_uri sent in the authorization request must be strictly identical to the one used during the server-side code exchange. Any discrepancy causes Keycloak to reject it.
The server must use the redirect_uri provided by the client during the callback, not a hardcoded value.
6.2 sessionStorage keys
All sessionStorage keys used in the OAuth flow (realm, client_id, state, code_verifier…) must be declared in a shared constants file across all involved modules. Scattered string literals are forbidden.
6.3 login_hint
When the user's email is known before redirecting to Keycloak, pass the standard OIDC login_hint parameter to pre-fill the Keycloak form and avoid double entry.
6.4 Multi-organization (Discovery)
If the Discovery API can return multiple accounts for the same email (multiple Keycloak realms), the frontend must display an organization selection screen before redirecting. Never blindly take users[0] if the response can contain multiple entries.
- Check for the presence of users with
users?.length > 0and not with afoundfield that may not exist depending on the API version. - Systematically verify the exact name of the identifier field returned by the API (
customer_widvsnetwork_id,customer_idvscustomer_wid…) and document it in the API contract.
6.5 Keycloak URL on the server side
The KEYCLOAK_URL variable (internal or public URL for server-side calls) must be explicitly defined in the K8s configuration. Without it, calls fall back to localhost:8080 and fail silently.
Document in .env.example the difference between KEYCLOAK_URL (server-side) and NEXT_PUBLIC_KEYCLOAK_URL / KEYCLOAK_PUBLIC_URL (client-side / runtime).
7. Prisma migrations (beyond runtime)
Database migrations must be managed deterministically and reproducibly across environments.
Rules for Prisma migrations
- The
prisma/migrations/directory must contain an initial migration generated byprisma migrate devthat creates the entire schema. Loose SQL scripts not managed by Prisma are not acceptable. - Specific migrations (e.g. FTS index, column additions) must be encapsulated in dedicated Prisma migrations, not as standalone SQL files.
prismamust be independencies(notdevDependencies) so thatprisma migrate deployis available in the production image.entrypoint.shrunsprisma migrate deploybefore any application startup — see section 2.- Never import
dotenv/configinprisma.config.ts: environment variables are injected by K8s.