Deployment
Deployment answers a different question from adapters. The adapter docs explain which runtime contract DaloyJS plugs into. This page helps you choose where to run a DaloyJS REST API and shows the packaging and platform config that matter in production.
- 01ciBuildpnpm install --frozen-lockfile, pnpm build
- 02Packagedistroless image or dist/
- 03Releasesigned image + SBOM attestation
- 04RunNode adapter, graceful shutdown
Node platforms
These providers all run the Node adapter. What changes is the platform config, health checks, and rollout mechanics.
Google Cloud Run
Cloud Run runs the Docker image below as-is. Three things matter:
- Listen on
$PORT. Cloud Run injects it (default8080):serve(app, { port: Number(process.env.PORT ?? 8080) }). - Shutdown budget. Cloud Run sends
SIGTERMand waits 10 seconds beforeSIGKILL. The Node adapter's defaultshutdownTimeoutMsis also 10 seconds, so lower it (for example to8_000) to finish draining before the kill. - Client IPs. Requests arrive through Google's front end, so set
behindProxy: { hops: 1 }. Add one hop for each extra proxy, for example an external Application Load Balancer in front of Cloud Run.
--allow-unauthenticated only makes the service reachable; your DaloyJS routes still do their own auth. Cloud Run functions (the former Cloud Functions) expect an Express-style (req, res) handler, which DaloyJS does not ship, so deploy the API as a Cloud Run service instead.
Production checklist
- Set
NODE_ENV=production(orenv: "production"). 5xxdetailis already redacted whenNODE_ENVis unset, but production mode also turns on the refuse-to-boot guards and turns off auto-mounted docs. - Keep the app-wide
bodyLimitBytessmall and raise it only on the routes that need more, with a per-routebodyLimitBytes(since 1.21.0). - Set
requestTimeoutMsto less than your load balancer's idle timeout. - Mount
rateLimit()globally.new App()already appliessecureHeaders()and a request id to every response; register your ownsecureHeaders()only to change its options. - Declare
behindProxyfor your platform (see Reverse proxy). Behind any proxy, leaving it unset makes production answer500to the first request that carriesX-Forwarded-For. - Register health and readiness probes with
app.healthcheck()andapp.readinesscheck()and point your platform's checks at them. - Wire your structured logger and propagate
request-idto downstream calls. - Run contract tests in CI, fail the build if the spec drifts.
- Use
pnpm install --frozen-lockfilein CI. Neverpnpm install.
Docker (Node, distroless)
Pin each base image by digest in production (node:24-bookworm@sha256:<digest>) so a re-pushed tag cannot change what you build. Distroless has no shell, so there is no HEALTHCHECK line: let the platform probe /healthz over HTTP instead. Prefer a HEALTHCHECK baked into the image? The create-daloy container templates ship an Alpine variant with tini as PID 1, STOPSIGNAL SIGTERM, a wget-based HEALTHCHECK, and digest-pinned bases.
Signed images + SBOM attestation
create-daloy --with-ci ships a deploy.yml that, after every successful push to GHCR, resolves the immutable @sha256:<digest>, signs the image with Sigstore Cosign (keyless / OIDC, no long-lived signing key), generates an SPDX SBOM for the image, and uploads it as a Cosign attestation (--type spdxjson). The job grants id-token: write alongside packages: write. The top-level workflow keeps permissions: {}. This closes the Aikido container-security checklist items for "Use Signed Images" and "Generate an SBOM." Verify any pulled image with:
Kubernetes
The same image runs on Kubernetes (EKS, GKE, AKS, or your own cluster). Wire the probes and the shutdown timing so a rollout never drops requests:
- Shutdown order. Kubernetes runs
preStop, then sendsSIGTERM, then kills the pod whenterminationGracePeriodSecondsruns out. The shortpreStopsleep gives Services and Ingress time to stop routing to the pod, thenserve()drains for up toshutdownTimeoutMs. KeeppreStopplusshutdownTimeoutMsunder the grace period. Thesleepaction needs Kubernetes 1.30 or newer; distroless images have nosleepbinary for anexechook. - Readiness while draining.
/readyzanswers503as soon as draining starts, so the pod leaves the load balancer even ifpreStopis skipped. - Probe auth. In production
app.healthcheck()refuses to register without atokenoracknowledgeUnauthenticated: true. A kubelet probe can send a fixedAuthorizationheader (httpHeaders), but that value sits in the pod spec. If you acknowledge unauthenticated probes instead, block/healthzand/readyzat your Ingress so they are not public. - Client IPs. One Ingress controller or cloud load balancer in front is
behindProxy: { hops: 1 }. Add a hop for each extra layer, such as a CDN in front of the Ingress.
Graceful shutdown
The Node adapter installs SIGTERM/SIGINT handlers by default. DaloyJS stops accepting new requests (returning 503) and waits up to shutdownTimeoutMs for in-flight requests to drain.
Reverse proxy
If you sit behind nginx / Caddy / a load balancer / a Kubernetes Ingress / a PaaS edge (Railway, Render, Fly, Heroku, Cloud Run), declare the proxy posture so DaloyJS resolves the real client IP and stops refusing forwarded requests:
- Set
behindProxy: { hops: N }onnew App({ ... }), whereNis the number of trusted proxy hops in front of the app (a single edge proxy is1. Cloudflare in front of one PaaS edge is2). In production an unconfigured posture makes DaloyJS return500on the first request carrying anX-Forwarded-*header, so a misconfigured chain cannot feed spoofable client IPs torateLimit(), request-id propagation, or audit logs. UsebehindProxy: "none"when the app faces the public internet directly. - Once the posture is declared,
rateLimit()keys on the resolved client IP automatically, no customkeyGeneratorrequired. X-Forwarded-Foris read by default. Addheader: "forwarded"for an RFC 7239Forwardedproxy (Traefik, Caddy), orheader: "cf-connecting-ip"behind Cloudflare, where the single-value header Cloudflare writes cannot be pre-populated by a client; see which header carries the client address.- Make the LB's idle timeout greater than DaloyJS's
requestTimeoutMs. - Make the server's
keepAliveTimeoutgreater than the LB's idle timeout, or the LB can reuse a socket Node just closed and return a502. The Node adapter sets it to 5 seconds, which is shorter than most load balancers (AWS ALB defaults to 60 seconds), so raise it on the handle:serve(app).server.keepAliveTimeout = 65_000.
Edge / serverless REST APIs
DaloyJS can run on Vercel Functions, Cloudflare Workers (or self-hosted workerd), Netlify Functions, AWS Lambda, Azure Functions, and Fastly Compute because the core is Web-standard Request -> Response. Deno Deploy also works until it shuts down around April 2027; see Moving off Deno Deploy. Use these targets when you want per-request billing or a managed edge/serverless runtime instead of a long-lived Node process.
For a standalone Vercel REST API, create a single api/index.ts function and export the web-standard fetch handler, plus a vercel.json rewrite so DaloyJS routes at the site root (without it the root domain 404s):
See Vercel, Cloudflare Workers, Netlify, AWS Lambda, Azure Functions, and Fastly Compute for platform-specific entry files.