| name | shipnode |
| description | Deploy Node.js apps to a single VPS using the shipnode CLI (zero-downtime releases, PM2, Caddy, SSH). Use when the user asks about deploying, setting up a server, rolling back, configuring Caddy/PM2, managing .env on a remote server, or running shipnode commands. |
shipnode
Quick start
shipnode init
shipnode setup
shipnode deploy
Config builder
shipnode.config.ts — backend app:
import { shipnode } from '@devalade/shipnode';
export default shipnode
.backend()
.ssh({ host: '1.2.3.4', user: 'deploy' })
.deployTo('/var/www/myapp')
.pm2('myapp', { instances: 2 })
.port(3000)
.domain('api.example.com')
.healthCheck('/health')
.nodeVersion('22')
.pkgManager('pnpm')
.build();
Frontend app: swap .backend() → .frontend(), add .buildDir('dist'), drop .pm2() / .port().
Workflows
First deploy to a new server
shipnode init — creates config
shipnode setup — installs Node/PM2/Caddy on server
shipnode env — uploads .env
shipnode deploy
Zero-downtime is the default
Releases live in /remotePath/releases/<timestamp>/. Current symlink is atomically switched.
To opt out: add .legacy() to the builder.
Rollback
shipnode rollback
shipnode rollback --steps 3
Database migrations in deploy
.preDeploy(async ({ exec }) => {
await exec('npx prisma migrate deploy');
})
CI/CD (GitHub Actions)
shipnode ci github
shipnode ci env-sync
Stuck deploy lock
shipnode unlock
Security hardening
shipnode doctor --security
shipnode harden
shipnode cloudflare init
Backups to S3
Add .backup({ s3Bucket, s3Prefix, schedule, retentionDays }) to config, then:
shipnode backup setup
shipnode backup run
shipnode backup list
Day-to-day commands
shipnode status
shipnode logs
shipnode logs --lines 500
shipnode restart
shipnode stop
shipnode run "npm run migrate"
shipnode run bash
shipnode metrics
shipnode deploy --dry-run
shipnode deploy --skip-build
See REFERENCE.md for the full command & config option reference.
Troubleshooting
Build fails on remote but works locally
Symptom: TypeScript or compilation errors appear during shipnode deploy that don't show up when running locally.
Cause: Locally you're likely running next dev, which skips type checking. The remote always runs next build, which runs the full TypeScript compiler. A newer TypeScript version resolved on the remote (fresh pnpm install) can also surface errors that an older local version missed.
Fix: Run pnpm build locally before deploying to catch the same errors the remote will see.
Prisma — Module '@prisma/client' has no exported member 'X' or implicit any on Prisma query results
Symptom: Build fails with a missing Prisma type export, or Prisma query results have implicit any type.
Cause: prisma generate was not run before next build. Without it the Prisma client types don't exist, so all Prisma results are untyped.
What doesn't work: Putting prisma generate in .preDeploy() runs it after the build — too late.
Fix: Add it to the build script in package.json so it runs before Next.js compiles:
"build": "prisma generate && next build"
For migrations, keep prisma migrate deploy in .preDeploy() — that's the right place since it needs the database to be reachable at deploy time, not at build time:
.preDeploy(async ({ exec }) => {
await exec('pnpm db:apply');
})
Port configuration — how it works
Shipnode sets the PORT environment variable in the PM2 ecosystem file based on the .port(n) value in shipnode.config.ts. PM2 passes it to the Node.js process at startup.
.port(3001) → PM2 env: { PORT: 3001 } → process.env.PORT = '3001'
Next.js 13.5+ reads process.env.PORT automatically when no -p flag is given. No changes are needed in package.json or the app code.
Multiple apps on one server: give each app a unique port in its config — Caddy proxies each domain to the correct port:
.port(3001)
.port(3002)