Node.js Deployment
Detection
Project is Node.js if package.json exists in the root directory.
Versions
Node.js version priority:
.node-version or .nvmrc file
engines.node field in package.json
mise.toml or .tool-versions
- Defaults to 22
Bun
If Bun is detected as the package manager:
engines.bun field in package.json
.bun-version file
mise.toml or .tool-versions
- Defaults to latest
When Bun is the primary runtime, Node.js must still be installed if:
- The project uses Corepack (
packageManager field exists in package.json)
- Build tools require Node (Astro, Vite, or native addon compilation via
node-gyp)
- Any
package.json script explicitly invokes node
Package Managers
Detect in order:
packageManager field in package.json → use Corepack to install exact version (e.g. pnpm@9.1.0)
- Lock files:
| Lock file |
Package manager |
package-lock.json |
npm |
yarn.lock |
Yarn (check .yarnrc.yml to distinguish Yarn Berry from Classic) |
pnpm-lock.yaml |
pnpm |
bun.lockb or bun.lock |
Bun |
engines field — engines.pnpm → pnpm, engines.bun → Bun, engines.yarn → Yarn
- Default: npm
Install Commands
| Package manager |
With lockfile |
Without lockfile |
| npm |
npm ci |
npm install |
| yarn (classic) |
yarn install --frozen-lockfile |
yarn install |
| yarn (berry) |
yarn install --immutable |
yarn install |
| pnpm |
pnpm install --frozen-lockfile |
pnpm install |
| bun |
bun install --frozen-lockfile |
bun install |
Runtime Variables
Set NODE_ENV=production for the runtime stage. During the build, keep NPM_CONFIG_PRODUCTION=false and YARN_PRODUCTION=false so dev dependencies remain available for compilation. Disable update notifications with NPM_CONFIG_UPDATE_NOTIFIER=false and NPM_CONFIG_FUND=false. Set CI=true to enable CI-appropriate behavior in tooling.
Build & Start
Start Command Resolution
start script in package.json
main or module field in package.json (run with node)
server.js, index.js, or index.ts in root (run with node)
Build Command Resolution
build script in package.json → ${packageManager} run build
- If no build script → skip build step
Output Directory
| Framework |
Output directory |
| NestJS |
dist |
| Next.js (SSR) |
.next |
| Next.js (export) |
out |
| Nuxt |
.output |
| SvelteKit |
build |
| Remix |
build |
| Astro |
dist |
| Vite |
dist |
| Angular |
dist/${projectName} |
| React (CRA) |
build |
| React Router |
build/client |
| Default |
dist |
Port Detection
- Environment files —
PORT=<number> from .env, .env.example, .env.production
package.json scripts — scan start, dev, serve for -p <port>, --port <port>, PORT=<port>
- Framework config —
next.config.* or vite.config.* for port: <number>
- Framework defaults:
| Framework |
Default port |
| Express |
3000 |
| Fastify |
3000 |
| NestJS |
3000 |
| Hono |
3000 |
| Next.js |
3000 |
| Nuxt |
3000 |
| Remix |
3000 |
| SvelteKit |
5173 |
| Astro |
4321 |
| Vite |
5173 |
| React |
3000 |
| Vue |
8080 |
- Final default: 3000
Framework Detection
From package.json dependencies (merge dependencies + devDependencies). First match wins.
| Package pattern |
Framework |
Category |
express |
Express |
Backend |
fastify |
Fastify |
Backend |
@nestjs/core |
NestJS |
Backend |
hono |
Hono |
Backend |
next |
Next.js |
FullStack |
nuxt |
Nuxt |
FullStack |
@sveltejs/kit |
SvelteKit |
FullStack |
@remix-run/node or @remix-run/react |
Remix |
FullStack |
astro |
Astro |
Static |
vite (without a higher framework) |
Vite |
Frontend |
react + react-dom (without Next/Remix) |
React |
Frontend |
vue (without Nuxt) |
Vue |
Frontend |
Config file fallback:
| Config file |
Framework |
nest-cli.json |
NestJS |
next.config.js, next.config.mjs, next.config.ts |
Next.js |
nuxt.config.js, nuxt.config.ts |
Nuxt |
svelte.config.js |
SvelteKit |
remix.config.js |
Remix |
astro.config.mjs, astro.config.js |
Astro |
vite.config.ts, vite.config.js |
Vite |
vue.config.js |
Vue |
angular.json |
Angular |
Framework-Specific Behavior
Next.js
- Check
next.config.* for output: "standalone" → standalone build (smaller image, includes node_modules subset)
output: "export" → static site, no server needed
- Cache
.next/cache between builds
app/ directory → React Server Components
Nuxt
- Default start:
node .output/server/index.mjs
- Cache
node_modules/.cache
Astro
- If
output is not "server" → static site
- Cache
node_modules/.astro
Monorepo Support
Detection Signals
workspaces field in root package.json
pnpm-workspace.yaml
- Build orchestrators:
turbo.json, nx.json, lerna.json, rush.json
- Conventional directories:
apps/, packages/, services/
Workspace Package Resolution
- pnpm: Parse
pnpm-workspace.yaml → packages: list
- npm / yarn / bun: Parse
workspaces field in root package.json
Build Steps
- Detect workspace configurations automatically
- Install all workspace dependencies (copy all
package.json files + root lock file)
- Respect workspace dependency links
- Cache workspace
node_modules
- Build the target workspace package
Optimizing the Install Layer
Always copy:
package.json (root + workspace packages if monorepo)
- Lock file
pnpm-workspace.yaml (if pnpm monorepo)
.npmrc (if exists — contains registry config)
Framework-specific install files (copy if they exist, they trigger postinstall):
prisma/schema.prisma — Prisma generates client on postinstall
.env files needed at build time (e.g. Next.js NEXT_PUBLIC_*)
If package.json defines preinstall or postinstall scripts that depend on source files, copy the entire source before install to avoid broken hooks.
Static Sites
| Framework |
Detection |
Default output dir |
| CRA |
react-scripts in deps |
build |
| Vite |
vite.config.js/ts or build script contains vite build |
dist |
| Angular |
angular.json |
dist/${projectName} |
| Astro |
astro.config.* and output is not "server" |
dist |
| Next.js (export) |
output: "export" in config |
out |
| React Router |
react-router.config.* (ssr: false for SPA) |
build/client |
Serve with Caddy/nginx. SPA fallback, cache headers for hashed assets, gzip/brotli.
Environment Variable Semantics
Build-Time vs Runtime
| Framework |
Build-time prefix |
Runtime access |
| Next.js |
NEXT_PUBLIC_* |
process.env.* (server only) |
| Nuxt |
NUXT_PUBLIC_* |
process.env.* via useRuntimeConfig() |
| Vite |
VITE_* |
not available at runtime (build-only) |
| SvelteKit |
PUBLIC_* |
$env/static/public (build-only) |
| Astro |
PUBLIC_* |
import.meta.env.* (build-only) |
| CRA |
REACT_APP_* |
not available at runtime (build-only) |
Build-time env vars must be available during Docker build step (via ARG + ENV).
System Dependencies
| Package |
Required system packages |
| Puppeteer |
Chromium, xvfb, font libraries, Chrome system deps |
| Playwright |
Chromium headless shell, system packages |
sharp |
libvips and build tools |
bcrypt |
python3, make, g++ |
canvas |
libcairo2-dev, libjpeg-dev, libpango1.0-dev, libgif-dev, build-essential |
Dev Dependency Pruning
After build, remove dev dependencies to reduce image size:
- npm:
npm prune --omit=dev
- yarn:
yarn install --production or set NODE_ENV=production during install
- pnpm:
pnpm prune --prod
- bun:
bun install --production
Skip pruning if the start command references a dev dependency (ts-node, tsx, nodemon).
Caching
| Framework |
Cache directory |
| NestJS |
node_modules/.cache |
| Next.js |
.next/cache |
| Nuxt |
node_modules/.cache |
| SvelteKit |
node_modules/.cache |
| Remix |
.cache |
| React Router |
.react-router |
| Astro |
node_modules/.astro |
| Vite |
node_modules/.vite |
| Default |
node_modules/.cache |
Dockerfile Patterns
Simple Node.js Server (Express, Fastify, NestJS, Hono)
FROM node:<version>-slim AS base
FROM base AS deps
WORKDIR /app
COPY package.json <lockfile> ./
RUN <install-command>
FROM base AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN <build-command>
FROM base AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./
EXPOSE <port>
CMD ["node", "dist/index.js"]
Next.js Standalone
FROM node:<version>-slim AS base
FROM base AS deps
WORKDIR /app
COPY package.json <lockfile> ./
RUN <install-command>
FROM base AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN <build-command>
FROM base AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
Static Site (Vite, CRA, Astro static)
FROM node:<version>-slim AS build
WORKDIR /app
COPY package.json <lockfile> ./
RUN <install-command>
COPY . .
RUN <build-command>
FROM caddy:alpine AS runtime
COPY --from=build /app/<output-dir> /srv
COPY Caddyfile /etc/caddy/Caddyfile
EXPOSE 80
Gotchas
- Yarn Berry (v2+) uses Plug'n'Play by default —
node_modules won't exist unless nodeLinker: node-modules is set in .yarnrc.yml
npm ci deletes node_modules before installing — copy package.json + lockfile first, install, then copy source for proper layer caching
- Next.js
output: "standalone" must be set in config BEFORE running the build — the build step generates the standalone directory
- Prisma runs
prisma generate on postinstall — prisma/schema.prisma must be copied before npm install
- pnpm with
--shamefully-hoist may be needed for packages expecting a flat node_modules layout
- Bun
--frozen-lockfile is the correct flag (not --ci like npm)
- SvelteKit and Astro dev servers use different ports (5173, 4321) than production — ensure EXPOSE matches the production port
1---2name: node-deploy3description: Build and deploy Node.js applications — version detection, package managers, framework-specific builds, monorepo support, and Dockerfile patterns. Use when deploying a Node.js, JavaScript, or TypeScript project, or when package.json is detected in the repository.4---56# Node.js Deployment78## Detection910Project is Node.js if `package.json` exists in the root directory.1112## Versions1314Node.js version priority:15161. `.node-version` or `.nvmrc` file172. `engines.node` field in `package.json`183. `mise.toml` or `.tool-versions`194. Defaults to **22**2021### Bun2223If Bun is detected as the package manager:24251. `engines.bun` field in `package.json`262. `.bun-version` file273. `mise.toml` or `.tool-versions`284. Defaults to **latest**2930When Bun is the primary runtime, Node.js must still be installed if:3132- The project uses Corepack (`packageManager` field exists in `package.json`)33- Build tools require Node (Astro, Vite, or native addon compilation via `node-gyp`)34- Any `package.json` script explicitly invokes `node`3536## Package Managers3738Detect in order:39401. **`packageManager` field** in `package.json` → use Corepack to install exact version (e.g. `pnpm@9.1.0`)412. **Lock files:**4243| Lock file | Package manager |44|---|---|45| `package-lock.json` | npm |46| `yarn.lock` | Yarn (check `.yarnrc.yml` to distinguish Yarn Berry from Classic) |47| `pnpm-lock.yaml` | pnpm |48| `bun.lockb` or `bun.lock` | Bun |49503. **`engines` field** — `engines.pnpm` → pnpm, `engines.bun` → Bun, `engines.yarn` → Yarn514. **Default:** npm5253### Install Commands5455| Package manager | With lockfile | Without lockfile |56|---|---|---|57| npm | `npm ci` | `npm install` |58| yarn (classic) | `yarn install --frozen-lockfile` | `yarn install` |59| yarn (berry) | `yarn install --immutable` | `yarn install` |60| pnpm | `pnpm install --frozen-lockfile` | `pnpm install` |61| bun | `bun install --frozen-lockfile` | `bun install` |6263## Runtime Variables6465Set `NODE_ENV=production` for the runtime stage. During the build, keep `NPM_CONFIG_PRODUCTION=false` and `YARN_PRODUCTION=false` so dev dependencies remain available for compilation. Disable update notifications with `NPM_CONFIG_UPDATE_NOTIFIER=false` and `NPM_CONFIG_FUND=false`. Set `CI=true` to enable CI-appropriate behavior in tooling.6667## Build & Start6869### Start Command Resolution70711. `start` script in `package.json`722. `main` or `module` field in `package.json` (run with `node`)733. `server.js`, `index.js`, or `index.ts` in root (run with `node`)7475### Build Command Resolution76771. `build` script in `package.json` → `${packageManager} run build`782. If no build script → skip build step7980### Output Directory8182| Framework | Output directory |83|---|---|84| NestJS | `dist` |85| Next.js (SSR) | `.next` |86| Next.js (export) | `out` |87| Nuxt | `.output` |88| SvelteKit | `build` |89| Remix | `build` |90| Astro | `dist` |91| Vite | `dist` |92| Angular | `dist/${projectName}` |93| React (CRA) | `build` |94| React Router | `build/client` |95| Default | `dist` |9697## Port Detection98991. **Environment files** — `PORT=<number>` from `.env`, `.env.example`, `.env.production`1002. **`package.json` scripts** — scan `start`, `dev`, `serve` for `-p <port>`, `--port <port>`, `PORT=<port>`1013. **Framework config** — `next.config.*` or `vite.config.*` for `port: <number>`1024. **Framework defaults:**103104| Framework | Default port |105|---|---|106| Express | 3000 |107| Fastify | 3000 |108| NestJS | 3000 |109| Hono | 3000 |110| Next.js | 3000 |111| Nuxt | 3000 |112| Remix | 3000 |113| SvelteKit | 5173 |114| Astro | 4321 |115| Vite | 5173 |116| React | 3000 |117| Vue | 8080 |1181195. **Final default:** 3000120121## Framework Detection122123From `package.json` dependencies (merge `dependencies` + `devDependencies`). First match wins.124125| Package pattern | Framework | Category |126|---|---|---|127| `express` | Express | Backend |128| `fastify` | Fastify | Backend |129| `@nestjs/core` | NestJS | Backend |130| `hono` | Hono | Backend |131| `next` | Next.js | FullStack |132| `nuxt` | Nuxt | FullStack |133| `@sveltejs/kit` | SvelteKit | FullStack |134| `@remix-run/node` or `@remix-run/react` | Remix | FullStack |135| `astro` | Astro | Static |136| `vite` (without a higher framework) | Vite | Frontend |137| `react` + `react-dom` (without Next/Remix) | React | Frontend |138| `vue` (without Nuxt) | Vue | Frontend |139140Config file fallback:141142| Config file | Framework |143|---|---|144| `nest-cli.json` | NestJS |145| `next.config.js`, `next.config.mjs`, `next.config.ts` | Next.js |146| `nuxt.config.js`, `nuxt.config.ts` | Nuxt |147| `svelte.config.js` | SvelteKit |148| `remix.config.js` | Remix |149| `astro.config.mjs`, `astro.config.js` | Astro |150| `vite.config.ts`, `vite.config.js` | Vite |151| `vue.config.js` | Vue |152| `angular.json` | Angular |153154### Framework-Specific Behavior155156**Next.js**157- Check `next.config.*` for `output: "standalone"` → standalone build (smaller image, includes `node_modules` subset)158- `output: "export"` → static site, no server needed159- Cache `.next/cache` between builds160- `app/` directory → React Server Components161162**Nuxt**163- Default start: `node .output/server/index.mjs`164- Cache `node_modules/.cache`165166**Astro**167- If `output` is not `"server"` → static site168- Cache `node_modules/.astro`169170## Monorepo Support171172### Detection Signals173174- `workspaces` field in root `package.json`175- `pnpm-workspace.yaml`176- Build orchestrators: `turbo.json`, `nx.json`, `lerna.json`, `rush.json`177- Conventional directories: `apps/`, `packages/`, `services/`178179### Workspace Package Resolution180181- **pnpm:** Parse `pnpm-workspace.yaml` → `packages:` list182- **npm / yarn / bun:** Parse `workspaces` field in root `package.json`183184### Build Steps1851861. Detect workspace configurations automatically1872. Install all workspace dependencies (copy all `package.json` files + root lock file)1883. Respect workspace dependency links1894. Cache workspace `node_modules`1905. Build the target workspace package191192## Optimizing the Install Layer193194**Always copy:**195- `package.json` (root + workspace packages if monorepo)196- Lock file197- `pnpm-workspace.yaml` (if pnpm monorepo)198- `.npmrc` (if exists — contains registry config)199200**Framework-specific install files** (copy if they exist, they trigger postinstall):201- `prisma/schema.prisma` — Prisma generates client on `postinstall`202- `.env` files needed at build time (e.g. Next.js `NEXT_PUBLIC_*`)203204If `package.json` defines `preinstall` or `postinstall` scripts that depend on source files, copy the entire source before install to avoid broken hooks.205206## Static Sites207208| Framework | Detection | Default output dir |209|---|---|---|210| CRA | `react-scripts` in deps | `build` |211| Vite | `vite.config.js/ts` or build script contains `vite build` | `dist` |212| Angular | `angular.json` | `dist/${projectName}` |213| Astro | `astro.config.*` and output is not `"server"` | `dist` |214| Next.js (export) | `output: "export"` in config | `out` |215| React Router | `react-router.config.*` (`ssr: false` for SPA) | `build/client` |216217Serve with Caddy/nginx. SPA fallback, cache headers for hashed assets, gzip/brotli.218219## Environment Variable Semantics220221### Build-Time vs Runtime222223| Framework | Build-time prefix | Runtime access |224|---|---|---|225| Next.js | `NEXT_PUBLIC_*` | `process.env.*` (server only) |226| Nuxt | `NUXT_PUBLIC_*` | `process.env.*` via `useRuntimeConfig()` |227| Vite | `VITE_*` | not available at runtime (build-only) |228| SvelteKit | `PUBLIC_*` | `$env/static/public` (build-only) |229| Astro | `PUBLIC_*` | `import.meta.env.*` (build-only) |230| CRA | `REACT_APP_*` | not available at runtime (build-only) |231232Build-time env vars must be available during Docker `build` step (via `ARG` + `ENV`).233234## System Dependencies235236| Package | Required system packages |237|---|---|238| Puppeteer | Chromium, xvfb, font libraries, Chrome system deps |239| Playwright | Chromium headless shell, system packages |240| `sharp` | `libvips` and build tools |241| `bcrypt` | `python3`, `make`, `g++` |242| `canvas` | `libcairo2-dev`, `libjpeg-dev`, `libpango1.0-dev`, `libgif-dev`, `build-essential` |243244## Dev Dependency Pruning245246After build, remove dev dependencies to reduce image size:247248- **npm:** `npm prune --omit=dev`249- **yarn:** `yarn install --production` or set `NODE_ENV=production` during install250- **pnpm:** `pnpm prune --prod`251- **bun:** `bun install --production`252253Skip pruning if the start command references a dev dependency (`ts-node`, `tsx`, `nodemon`).254255## Caching256257| Framework | Cache directory |258|---|---|259| NestJS | `node_modules/.cache` |260| Next.js | `.next/cache` |261| Nuxt | `node_modules/.cache` |262| SvelteKit | `node_modules/.cache` |263| Remix | `.cache` |264| React Router | `.react-router` |265| Astro | `node_modules/.astro` |266| Vite | `node_modules/.vite` |267| Default | `node_modules/.cache` |268269## Dockerfile Patterns270271### Simple Node.js Server (Express, Fastify, NestJS, Hono)272273```dockerfile274FROM node:<version>-slim AS base275276FROM base AS deps277WORKDIR /app278COPY package.json <lockfile> ./279RUN <install-command>280281FROM base AS build282WORKDIR /app283COPY --from=deps /app/node_modules ./node_modules284COPY . .285RUN <build-command>286287FROM base AS runtime288WORKDIR /app289ENV NODE_ENV=production290COPY --from=build /app/dist ./dist291COPY --from=build /app/node_modules ./node_modules292COPY --from=build /app/package.json ./293EXPOSE <port>294CMD ["node", "dist/index.js"]295```296297### Next.js Standalone298299```dockerfile300FROM node:<version>-slim AS base301302FROM base AS deps303WORKDIR /app304COPY package.json <lockfile> ./305RUN <install-command>306307FROM base AS build308WORKDIR /app309COPY --from=deps /app/node_modules ./node_modules310COPY . .311RUN <build-command>312313FROM base AS runtime314WORKDIR /app315ENV NODE_ENV=production316COPY --from=build /app/.next/standalone ./317COPY --from=build /app/.next/static ./.next/static318COPY --from=build /app/public ./public319EXPOSE 3000320CMD ["node", "server.js"]321```322323### Static Site (Vite, CRA, Astro static)324325```dockerfile326FROM node:<version>-slim AS build327WORKDIR /app328COPY package.json <lockfile> ./329RUN <install-command>330COPY . .331RUN <build-command>332333FROM caddy:alpine AS runtime334COPY --from=build /app/<output-dir> /srv335COPY Caddyfile /etc/caddy/Caddyfile336EXPOSE 80337```338339## Gotchas340341- Yarn Berry (v2+) uses Plug'n'Play by default — `node_modules` won't exist unless `nodeLinker: node-modules` is set in `.yarnrc.yml`342- `npm ci` deletes `node_modules` before installing — copy `package.json` + lockfile first, install, then copy source for proper layer caching343- Next.js `output: "standalone"` must be set in config BEFORE running the build — the build step generates the standalone directory344- Prisma runs `prisma generate` on `postinstall` — `prisma/schema.prisma` must be copied before `npm install`345- pnpm with `--shamefully-hoist` may be needed for packages expecting a flat `node_modules` layout346- Bun `--frozen-lockfile` is the correct flag (not `--ci` like npm)347- SvelteKit and Astro dev servers use different ports (5173, 4321) than production — ensure EXPOSE matches the production port