Node Deploy
DevOps & Securitynode-deployNode Deploy is an Agent Skill for devops & security published in the nixopus/nixopus repository on GitHub: a SKILL.md instruction file an AI agent can load to do this task.
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.
How to use this skill
Bring this guide into your coding agent with a prompt tailored to the tool you use.
- Open your project in Codex.
- Copy and paste the prompt below.
- Review files and risks before approving any action.
Inspect the Agent Skill at https://github.com/nixopus/nixopus/blob/HEAD/api/skills/node-deploy/SKILL.md and all supporting files. Report provenance, permissions, scripts, dependencies, and security risks. Propose an exact installation into .agents/skills/node-deploy, but wait for my approval before writing files or running scripts.
Copying this prompt does not install or run the skill. Review third-party files before use.
Node.js Deployment
Detection
Project is Node.js if package.json exists in the root directory.
Versions
Node.js version priority:
.node-versionor.nvmrcfileengines.nodefield inpackage.jsonmise.tomlor.tool-versions- Defaults to 22
Bun
If Bun is detected as the package manager:
engines.bunfield inpackage.json.bun-versionfilemise.tomlor.tool-versions- Defaults to latest
When Bun is the primary runtime, Node.js must still be installed if:
- The project uses Corepack (
packageManagerfield exists inpackage.json) - Build tools require Node (Astro, Vite, or native addon compilation via
node-gyp) - Any
package.jsonscript explicitly invokesnode
Package Managers
Detect in order:
packageManagerfield inpackage.json→ use Corepack to install exact version (e.g.[email protected])- 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 |
enginesfield —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
startscript inpackage.jsonmainormodulefield inpackage.json(run withnode)server.js,index.js, orindex.tsin root (run withnode)
Build Command Resolution
buildscript inpackage.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.jsonscripts — scanstart,dev,servefor-p <port>,--port <port>,PORT=<port>- Framework config —
next.config.*orvite.config.*forport: <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.*foroutput: "standalone"→ standalone build (smaller image, includesnode_modulessubset) output: "export"→ static site, no server needed- Cache
.next/cachebetween builds app/directory → React Server Components
Nuxt
- Default start:
node .output/server/index.mjs - Cache
node_modules/.cache
Astro
- If
outputis not"server"→ static site - Cache
node_modules/.astro
Monorepo Support
Detection Signals
workspacesfield in rootpackage.jsonpnpm-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
workspacesfield in rootpackage.json
Build Steps
- Detect workspace configurations automatically
- Install all workspace dependencies (copy all
package.jsonfiles + 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 onpostinstall.envfiles needed at build time (e.g. Next.jsNEXT_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 --productionor setNODE_ENV=productionduring 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_moduleswon't exist unlessnodeLinker: node-modulesis set in.yarnrc.yml npm cideletesnode_modulesbefore installing — copypackage.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 generateonpostinstall—prisma/schema.prismamust be copied beforenpm install - pnpm with
--shamefully-hoistmay be needed for packages expecting a flatnode_moduleslayout - Bun
--frozen-lockfileis the correct flag (not--cilike npm) - SvelteKit and Astro dev servers use different ports (5173, 4321) than production — ensure EXPOSE matches the production port