WakaStart
Best practices

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.

Version v1.010 min read

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

IconAttributeRequirement
📝Example valueProvide a real, functional example value. No empty placeholder like xxx or changeme.
📌Contextual descriptionExplain in one sentence why this variable is needed for the application to work.
🔁Cross-service duplicateIf 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 optionalClearly state whether the variable is required or optional, and describe the impact of omitting it (degraded behavior, fatal error, disabled feature…).
🌐URL typeIf 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 onlyIf the variable is only used in dev, state this explicitly and explain why it isn't needed in prod.
🔐SecretIf 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 impactIf 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 .env file 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:

dockerfile
FROM 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

  • ✅ prisma must be listed in dependencies (and not devDependencies) to be available when the production container runs.
  • 🚨 Never use pnpm prune --prod without having verified that prisma, prisma generate, and all required native binaries (bcrypt, multer…) are in the production dependencies.

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 seed runs the command defined in the prisma.seed block. 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 than create, 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 in dependencies if 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.

RuleDetail
rootDir required in tsconfig.build.jsonAlways 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 rootDirNever 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 productionmulter, 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 dotenvDo 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.sh must 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 via process.env.NEXT_PUBLIC_*.
  • Document in each service's .env.example which 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.sh that reads nonexistent variables leaves window.__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 (443 for https, 80 for http), and not force the default port 9000 (which is the local MinIO port, not the OVH Object Storage port).
  • Document in .env.example whether the MINIO_ENDPOINT / S3_ENDPOINT variable 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 > 0 and not with a found field that may not exist depending on the API version.
  • Systematically verify the exact name of the identifier field returned by the API (customer_wid vs network_id, customer_id vs customer_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 by prisma migrate dev that 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.
  • prisma must be in dependencies (not devDependencies) so that prisma migrate deploy is available in the production image.
  • entrypoint.sh runs prisma migrate deploy before any application startup — see section 2.
  • Never import dotenv/config in prisma.config.ts: environment variables are injected by K8s.