node-deploy
GitHub提供 Node.js/JS/TS 项目的构建与部署指南,涵盖版本检测、包管理器识别、环境变量配置及主流框架的构建启动逻辑。
Trigger Scenarios
Install
npx skills add nixopus/nixopus --skill node-deploy -g -y
SKILL.md
Frontmatter
{
"name": "node-deploy",
"metadata": {
"version": "1.0"
},
"description": "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."
}
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.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 |
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
Version History
- cf05d97 Current 2026-08-20 14:52


