Node.js with Silo: npm, yarn, pnpm, and version pinning
Running Node under Silo — persisting node_modules with silo build, switching Node versions per project, and wiring yarn / pnpm via corepack or custom shims.
This is the long-form guide for the JavaScript ecosystem under Silo. Four topics:
- Running
node/npmand what survives between invocations. - Making dependencies persist — project
node_modulesvs.silo build. - Running different Node versions globally or per project.
- Wiring yarn, pnpm, and other package managers.
Prerequisites: Silo installed, ~/.silo/bin on your PATH. If not yet, start with Getting started.
Install Node
# Default (22-slim — current LTS track)
silo install node
# Specific major
silo install node@20
silo install node@18
Registry versions today: 23 (latest), 22 (LTS, default), 20, 18. All -slim variants.
After install, the node, npm, and npx shims land in ~/.silo/bin/. Because that’s on your PATH, every node, npm, or npx you type transparently routes through Silo.
Running Node
node --version
node -e "console.log('hello from the VM')"
node server.js
npm --version
npx create-vite@latest my-app
Interactive:
silo shell node # shell inside the VM
silo node # REPL
Tool shorthand:
silo node server.js # == silo run node -- server.js
silo npm test # == silo run node --shim npm -- test
silo npx tsc --watch # == silo run node --shim npx -- tsc --watch
What persists, what doesn’t
Every silo run is a fresh VM. Understanding what survives is the whole game with the Node ecosystem, which loves dropping files on disk.
| Thing | Persists? | Where |
|---|---|---|
Project files (including node_modules/ if unignored) | Yes — mounted rw | Host disk |
Global npm install -g target | No by default | Ephemeral VM |
~/.npm (npm download cache) | Yes — automatic mount | ~/.silo/cache/node/npm |
| yarn cache | Only if configured | See below |
| pnpm store | Only if configured | See below |
Three strategies for persisting deps. Pick based on what you’re doing:
Option 1: node_modules/ in the project (the default)
Because your project directory is mounted rw, npm install creates a real node_modules/ on your host disk. It persists. Nothing to configure.
# .siloconf — give npm network access
tools: [node]
overrides:
node:
network:
hostAccess: true
proxy:
allow:
- registry.npmjs.org
- "*.npmjs.org"
- "*.github.com"
npm install
npm run dev
This is the default workflow and covers 90% of cases. The catch: node_modules/ on macOS under APFS with hundreds of thousands of small files from a cross-VFS mount can be slower than native. In practice it’s fine for dev; if you’re on a massive monorepo and notice it, jump to option 2 or 3.
Excluding node_modules from the mount
If you’d rather not see node_modules/ on the host at all (faster I/O, no cross-VFS overhead), tell Silo to hide it:
mount:
exclude:
- node_modules
Now node_modules/ lives entirely inside the VM and vanishes on exit — which means you need option 2 or 3 to make it persist.
Option 2: persist with silo build
silo build runs a command inside the VM and captures the resulting filesystem as a rootfs layer. Use it to bake npm install’s output into the image.
# .siloconf
tools: [node]
mount:
exclude:
- node_modules
overrides:
node:
network:
hostAccess: true
proxy:
allow:
- registry.npmjs.org
- "*.npmjs.org"
silo build node -- npm install
After this, every silo run node for this project starts with /workspace/node_modules/ already populated — but because of the mount.exclude, it’s the VM’s node_modules, not your host’s. The host directory stays clean.
Future runs:
node -e "require('express')" # works, no npm install needed
Iterating:
silo build node --rerun # re-run stored npm install
silo build node --rerun --script "npm ci" # override the script
silo build node --remove # throw out the layer, back to stock
silo setupis kept as a deprecated alias ofsilo buildin 0.4.x. Both work;setupgoes away in 0.6.
Global installs
# Globally available — every project sees typescript
silo build node --global -- npm install -g typescript
# Project-local stacks on top of global
silo build node -- npm install
Lookup order: project rootfs → global build rootfs → rootfs cache → OCI unpack.
When each option wins
- Project
node_modules/on host — default; works with every editor, every CI. Start here. silo build+mount.exclude— reproducible builds, faster I/O for huge monorepos, hermetic CI images.- Global
silo build— tools you want everywhere:typescript,prettier,eslint,vercel,netlify,wrangler.
Using different Node versions
Install an extra version globally
silo install node@20 # replaces the global node
Silo refuses a second silo install node unless you --force. The install key is the tool name (node), so installing node@20 replaces the global definition.
Pin a version for one project (recommended)
cd ~/projects/legacy-frontend
silo use node@18
silo sync # install anything missing + warm cache
node --version # v18.x — only inside this project
Outside this project, the global version is in effect. Walk into a project with a different pin and you’ll get its version automatically — no .nvmrc-style rehash per shell.
silo use writes to .siloconf:
tools: [node]
overrides:
node:
image: docker.io/library/node:18-slim
Undo with silo unuse node.
Manual override
Edit .siloconf directly if you want a version tag not in the registry:
overrides:
node:
image: docker.io/library/node:22-alpine
Any image tag works. Silo doesn’t care whether it’s in the built-in registry — that list just drives the --available display and silo install <tool>@<version> shorthand.
Migrating from nvm / volta / fnm
| nvm / volta / fnm | Silo |
|---|---|
nvm install 20 / volta install node@20 | silo install node@20 |
nvm use 18 (shell) | walk into the project with .siloconf pinning 18 |
.nvmrc / volta.node in package.json | .siloconf with overrides.node.image |
nvm alias default 22 | silo install node@22 --force |
Same mental model, plus a real sandbox.
yarn, pnpm, bun
The default node tool only ships node, npm, npx shims. yarn and pnpm don’t exist out of the box. There are two good ways to get them.
Corepack (recommended)
Modern Node ships Corepack, which manages yarn and pnpm on demand. Enable it once in a silo build and both are available inside the VM:
silo build node -- sh -c "corepack enable && corepack prepare pnpm@latest --activate && corepack prepare yarn@stable --activate"
Then add host shims so you can type yarn and pnpm directly:
silo shim node add yarn
silo shim node add pnpm
Now:
pnpm install
pnpm run dev
yarn install
If your package.json has a packageManager field ("packageManager": "pnpm@9.0.0"), corepack will pin to exactly that version automatically. No version drift between teammates.
Global install with npm
Simpler, less clever:
silo build node --global -- npm install -g yarn pnpm
silo shim node add yarn
silo shim node add pnpm
Works the same way. Corepack’s advantage is per-project pinning via packageManager; without that, global npm is fine.
bun
Bun isn’t in the default registry, but it’s one silo install away:
silo install bun --image oven/bun:latest --shim bun,bunx --network
Or add it to ~/.silo/registry.yaml so it lives alongside the built-ins.
Custom command mappings
silo shim supports host_name:container_command if the shim name differs from the binary:
silo shim node add npm2:npm # `npm2` on host runs `npm` in container
silo shim node add pnpx:pnpm dlx # `pnpx foo` runs `pnpm dlx foo`
Useful for avoiding conflicts when you temporarily want two Node installs side by side.
Networking for npm / yarn / pnpm
All three need outbound access. The allowlist is the same:
overrides:
node:
network:
hostAccess: true
proxy:
allow:
- registry.npmjs.org
- "*.npmjs.org"
- "*.github.com" # many packages download binaries from GitHub releases
- "*.cloudfront.net" # electron, prebuilt binaries
If you use a private registry, add it:
- npm.mycompany.com
- npm.pkg.github.com
For yarn/pnpm specifically you might also need:
- yarnpkg.com
- registry.yarnpkg.com
- "*.pnpm.io"
The proxy is allowlist-first: everything not in allow is blocked. That’s the whole point — a compromised postinstall script can’t phone home to attacker.example.
Passing .npmrc
Private registry auth usually lives in ~/.npmrc or ./.npmrc. Mount it read-only:
passFiles:
- .npmrc
Or pass the auth token as env:
passEnv:
- NPM_TOKEN
- GITHUB_TOKEN
Forwarding dev-server ports
Vite, Next, webpack-dev-server, etc. — map the ports:
overrides:
node:
ports:
- host: 3000
guest: 3000
- host: 5173 # Vite HMR
guest: 5173
Shorthand: silo config ports add node 3000:3000. Ports imply hostAccess: true.
Recipes
Next.js app
# .siloconf
tools: [node]
passEnv:
- DATABASE_URL
- NEXTAUTH_SECRET
mount:
exclude:
- node_modules
- .next
overrides:
node:
image: docker.io/library/node:20-slim
network:
hostAccess: true
proxy:
allow:
- registry.npmjs.org
- "*.npmjs.org"
- "*.vercel.com"
ports:
- host: 3000
guest: 3000
silo build node -- npm install
npm run dev
Vite + React with pnpm
silo build node -- sh -c "corepack enable && corepack prepare pnpm@latest --activate"
silo shim node add pnpm
# .siloconf
tools: [node]
mount:
exclude:
- node_modules
overrides:
node:
network:
hostAccess: true
proxy:
allow:
- registry.npmjs.org
- "*.npmjs.org"
- "*.github.com"
ports:
- host: 5173
guest: 5173
silo build node -- pnpm install
pnpm dev
Playwright browser tests
Silo ships a dedicated playwright tool that includes the browser binaries:
silo install playwright
silo build playwright -- npx playwright install --with-deps
npx playwright test
This avoids downloading ~400 MB of browser binaries into your project on every install.
Common pitfalls
- “Cannot find module” after restart — you ran
npm installin onesilo runand expected it to survive. Either putnode_modulesin the host-mounted project, or usesilo build node -- npm install. - Vite HMR not reloading — Vite uses a filesystem watcher; container inotify needs
--hoston the dev server and the port forwarded. Make surenetwork.hostAccess: trueand the port is in theportslist. npm installhangs — network probably isn’t allowlisted. Checkregistry.npmjs.orgis inproxy.allow.- Corepack says “not enabled” — you need to rerun
corepack enableinside the VM; it modifies a few scripts under/usr/local/bin, and those modifications need to be captured bysilo build.
Where to go next
- Python with Silo — same patterns for Python.
- Understanding .siloconf — where this config file lives, how it merges, and every field that matters.
- How Silo works — what’s actually happening under each
silo run.