Search docs

Jump between documentation pages.

Browse docs

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.

Build to run
  1. 01ciBuildpnpm install --frozen-lockfile, pnpm build
  2. 02Packagedistroless image or dist/
  3. 03Releasesigned image + SBOM attestation
  4. 04RunNode adapter, graceful shutdown
The same pipeline underpins every Node platform below. Only the packaging and platform config differ; the build, release, and run stages stay the same.

Node platforms

These providers all run the Node adapter. What changes is the platform config, health checks, and rollout mechanics.

Production checklist

  • Set NODE_ENV=production so 5xx detail is redacted.
  • Set a sane bodyLimitBytes per route group (don't default to 1 MiB everywhere).
  • Set requestTimeoutMs to less than your load balancer's idle timeout.
  • Mount secureHeaders(), requestId(), and rateLimit() globally.
  • Wire your structured logger and propagate request-id to downstream calls.
  • Run contract tests in CI, fail the build if the spec drifts.
  • Use pnpm install --frozen-lockfile in CI; never pnpm install.

Docker (Node, distroless)

dockerfile
# syntax=docker/dockerfile:1
    FROM node:24-bookworm AS deps
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml .npmrc ./
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
    pnpm install --frozen-lockfile --prod

    FROM node:24-bookworm AS build
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml .npmrc ./
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
    pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

    FROM gcr.io/distroless/nodejs24-debian12 AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps  /app/node_modules ./node_modules
COPY --from=build /app/dist          ./dist
COPY package.json ./
USER 1000
EXPOSE 3000
CMD ["dist/server.js"]

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:

bash
# Replace owner/repo and the digest with your values.
cosign verify ghcr.io/<owner>/<repo>@sha256:<digest> \
  --certificate-identity-regexp 'https://github\.com/<owner>/<repo>/\.github/workflows/deploy\.yml@.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

cosign verify-attestation \
  --type spdxjson \
  --certificate-identity-regexp 'https://github\.com/<owner>/<repo>/\.github/workflows/deploy\.yml@.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/<owner>/<repo>@sha256:<digest>

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.

ts
const { close } = serve(app, {
  shutdownTimeoutMs: 15_000,
  handleSignals: true,
});

// or trigger manually:
await app.shutdown(15_000);

Reverse proxy

If you sit behind nginx / Caddy / a load balancer / a PaaS edge (Railway, Render, Fly, Heroku), declare the proxy posture so DaloyJS resolves the real client IP and stops refusing forwarded requests:

  • Set behindProxy: { hops: N } on new App({ ... }), where N is the number of trusted proxy hops in front of the app (a single edge proxy is 1; Cloudflare in front of one PaaS edge is 2). In production an unconfigured posture makes DaloyJS return 500 on the first request carrying an X-Forwarded-* header, so a misconfigured chain cannot feed spoofable client IPs to rateLimit(), request-id propagation, or audit logs. Use behindProxy: "none" when the app faces the public internet directly.
  • Once the posture is declared, rateLimit() keys on the resolved client IP automatically, no custom keyGenerator required.
  • Make the LB's idle timeout greater than DaloyJS's requestTimeoutMs.
  • Make DaloyJS's keepAliveTimeout greater than the LB's, Node adapter does this for you.

Edge / serverless REST APIs

DaloyJS can run on Vercel Functions, Cloudflare Workers, Netlify Functions, AWS Lambda, Fastly Compute, and Deno Deploy because the core is Web-standard Request → Response. 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):

ts
// api/index.ts
import { toFetchHandler } from "@daloyjs/core/vercel";
import { app } from "../src/server.js";

export default toFetchHandler(app);

// vercel.json
// { "rewrites": [{ "source": "/(.*)", "destination": "/api" }] }

See Vercel, Cloudflare Workers, Netlify, AWS Lambda, and Fastly Compute for platform-specific entry files.