Docker
The first stage installs dependencies and builds the app.
The first stage installs dependencies and builds the app. The final stage installs production dependencies and copies the server output. Keep @shadow-js/core, @shadow-js/web, @shadow-js/router and @shadow-js/dusk in dependencies, and Vite and TypeScript in devDependencies.
Build and run the image in your deployment environment before rollout. Keep .env*, .git and node_modules out of the build context, run as an unprivileged user, and make the health-check port match the server's PORT.
# syntax=docker/dockerfile:1
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# A .dockerignore must exclude .env*, .git and node_modules, or this copies local secrets into the layer.
COPY . .
# DUSK_PUBLIC_* variables are inlined here; pass them as build arguments.
ARG DUSK_PUBLIC_API_URL
ENV DUSK_PUBLIC_API_URL=$DUSK_PUBLIC_API_URL
RUN npx dusk build
FROM node:22-alpine AS run
WORKDIR /app
ENV HOST=0.0.0.0 PORT=3000
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
COPY serve.mjs ./
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/_dusk/health').then((r)=>process.exit(r.ok?0:1),()=>process.exit(1))"
CMD ["node", "serve.mjs"]
The server only reads its files, so the unprivileged node user of the official image can run it. Secrets
(SESSION_SECRET and the like) are run-time variables (docker run -e or your platform's secret store), never
ARGs or ENV lines of the image. CMD ["node", "serve.mjs"] makes node the main process, so it receives SIGTERM
from docker stop; a shell wrapper would not forward it.