Deploy Node.js apps to your own server with a single command. No Kubernetes, no Docker, no vendor lock-in.
shipnode init && shipnode deployShipNode deploys your Node.js backend or static frontend to any Ubuntu/Debian server over SSH. It handles building, syncing files, process management, reverse proxying, and HTTPS.
Your Laptop Your Server
────────────── ────────────
shipnode deploy ───rsync──▶ /var/www/myapp/
├── current/ ← active release (symlink)
├── releases/ ← timestamped versions
├── shared/.env ← persistent config
└── PM2 + Caddy ← process + HTTPS
What gets installed on your server (shipnode setup):
- Node.js - LTS version via NodeSource
- PM2 - Process manager (auto-restart, crash recovery)
- Caddy - Web server with automatic HTTPS (Let's Encrypt)
- Databases - optional PostgreSQL, MySQL, SQLite, and Redis setup when enabled
curl -fsSL https://github.com/devalade/shipnode/releases/latest/download/shipnode-installer.sh -o shipnode-installer.sh
chmod +x shipnode-installer.sh
./shipnode-installer.shOr clone and run directly:
git clone https://github.com/devalade/shipnode.git
cd shipnode
./shipnode helpSee Installation for details. Uninstall: rm -rf ~/.shipnode
ShipNode includes a shareable AI agent skill that helps users plan deployments, write shipnode.conf, troubleshoot failed deploys, manage .env, roll back releases, and set up CI/CD.
Install it with the skills CLI:
npx skills add devalade/shipnodeAfter installing, ask your agent to use $shipnode when you want help deploying a Node.js app with ShipNode.
cd /path/to/your/project
shipnode initThe interactive wizard auto-detects your framework, suggests defaults, and creates shipnode.conf:
╔════════════════════════════════════╗
║ ShipNode Interactive Setup ║
╚════════════════════════════════════╝
→ Detected framework: Express
→ Suggested app type: backend
Application type:
1) Backend (Node.js API with PM2)
2) Frontend (Static site)
Choose [1-2] (detected: backend):
SSH user [root]:
SSH host (IP or hostname): 203.0.113.10
SSH port [22]:
Remote deployment path [/var/www/myapp]:
PM2 process name [myapp]:
Application port [3000]:
Domain (optional): api.myapp.com
════════════════════════════════════
Configuration Summary
════════════════════════════════════
App Type: backend
SSH: root@203.0.113.10:22
Remote Path: /var/www/myapp
PM2 Name: myapp
Backend Port: 3000
Domain: api.myapp.com
Zero-downtime: true
Health Checks: /health (30s timeout, 3 retries)
════════════════════════════════════
Create shipnode.conf with these settings? (Y/n):
Non-interactive mode for scripts/CI:
shipnode init --non-interactiveshipnode setupInstalls Node.js, PM2, and Caddy on your server. Run once per server.
shipnode deployYour app is live. That's it.
shipnode statusDeploy Express, NestJS, Next.js, AdonisJS, or any Node.js backend.
- Add a
/healthendpoint to your app:
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});shipnode deployWhat happens:
- Syncs code via rsync (excludes
node_modules,.env,.git) - Installs dependencies and builds on the server
- Creates a timestamped release directory
- Atomically switches
currentsymlink to new release (zero downtime) - Reloads PM2 gracefully
- Runs a health check against
/health - On failure: automatically rolls back to previous release
HEALTH_CHECK_ENABLED=true # default: true
HEALTH_CHECK_PATH=/health # default: /health
HEALTH_CHECK_TIMEOUT=30 # seconds per attempt (default: 30)
HEALTH_CHECK_RETRIES=3 # attempts before rollback (default: 3)Deploy React, Vue, Svelte, or any static SPA.
APP_TYPE=frontend
SSH_USER=root
SSH_HOST=123.45.67.89
REMOTE_PATH=/var/www/myapp
DOMAIN=myapp.comshipnode deployWhat happens:
- Builds your app locally (
npm run build) - Syncs the build output (
dist/,build/, orpublic/) to the server - Atomically switches the symlink
- Caddy serves static files with SPA routing and aggressive caching
Skip the build step if files are pre-built:
shipnode deploy --skip-buildIf a deployment fails health checks or you need to revert:
shipnode rollback # previous release
shipnode rollback 2 # 2 releases back
shipnode releases # see all releases first/var/www/myapp/
├── current -> releases/20260408143022/ ← always points to active release
├── releases/
│ ├── 20260408120000/ ← previous release (for rollback)
│ └── 20260408143022/ ← current release
├── shared/
│ ├── .env ← persistent env vars
│ └── ecosystem.config.cjs ← PM2 config
└── .shipnode/
├── releases.json ← deployment history
└── deploy.lock ← prevents concurrent deploys
Use different config files for different environments:
shipnode deploy # uses shipnode.conf (production)
shipnode deploy --profile staging # uses shipnode.staging.conf
shipnode deploy --config shipnode.prod.conf # uses custom config fileShipNode uses timestamped releases with atomic symlink switching (same pattern as Capistrano).
1. Acquire lock
2. Create release directory releases/20260408143022/
3. rsync files laptop → server
4. Link shared .env shared/.env → releases/.../ .env
5. Install dependencies npm install
6. Build (if needed) npm run build
7. Run pre-deploy hook migrations, cache warm, etc.
8. Atomic symlink switch current → releases/20260408143022/
9. Reload PM2 graceful reload, zero downtime
10. Health check GET localhost:3000/health (3 retries)
11. Record release timestamp, git commit, duration
12. Run post-deploy hook notifications, cache clear, etc.
13. Cleanup old releases keep last 5 by default
14. Release lock
If step 10 fails, ShipNode immediately reverts to the previous release.
Every deployment is recorded in .shipnode/releases.json:
{
"timestamp": "20260408143022",
"date": "2026-04-08T14:30:22Z",
"status": "success",
"duration_seconds": 45,
"commit": "abc1234",
"previous_release": "20260408120000",
"health_check": {
"passed": true,
"attempts": 1,
"response_time_ms": 23
}
}ShipNode generates PM2 and Caddy configs automatically. Eject to customize:
shipnode eject # eject PM2 + Caddy templates
shipnode eject pm2 # eject only PM2 template
shipnode eject caddy # eject only Caddy templateThis creates editable templates in .shipnode/templates/:
.shipnode/templates/
├── ecosystem.config.cjs ← customize PM2: cluster mode, memory limits
└── Caddyfile.caddy ← customize Caddy: headers, TLS, rate limiting
Ejected templates are preserved across deploys.
Templates use {{VAR}} placeholders replaced on every deploy:
| Variable | Description |
|---|---|
{{APP_NAME}} |
PM2 process name |
{{INTERPRETER}} |
Package manager (npm, yarn, pnpm, bun) |
{{REMOTE_PATH}} |
Deployment path on server |
{{BACKEND_PORT}} |
Application port |
{{DOMAIN}} |
Your domain name |
{{SERVE_PATH}} |
Path to static files (frontend) |
module.exports = {
apps: [{
name: "{{APP_NAME}}",
script: "{{INTERPRETER}}",
args: "start",
cwd: "{{REMOTE_PATH}}/current",
instances: "max", // use all CPU cores
exec_mode: "cluster", // enable cluster mode
max_memory_restart: "1G", // restart if memory exceeds 1GB
env: {
NODE_ENV: "production",
PORT: {{BACKEND_PORT}}
}
}]
};{{DOMAIN}} {
reverse_proxy localhost:{{BACKEND_PORT}}
encode gzip
rate_limit {
zone dynamic_zone {
key {remote_host}
events 100
window 1s
}
}
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
Strict-Transport-Security "max-age=31536000; includeSubDomains"
}
log {
output file /var/log/caddy/{{APP_NAME}}.log
format json
}
}
rm .shipnode/templates/ecosystem.config.cjs
rm .shipnode/templates/Caddyfile.caddyRuns before the new release goes live. If it fails, deployment aborts and rolls back.
#!/bin/bash
# Available: RELEASE_PATH, REMOTE_PATH, PM2_APP_NAME, BACKEND_PORT, SHARED_ENV_PATH
set -e
source "$SHARED_ENV_PATH" # load .env variables
cd "$RELEASE_PATH"
# Prisma migrations (auto-detected by shipnode init)
npx prisma generate
npx prisma migrate deployRuns after deployment succeeds. Failures don't affect deployment status.
#!/bin/bash
set -e
source "$SHARED_ENV_PATH"
# Send Slack notification
curl -X POST "$SLACK_WEBHOOK" \
-H 'Content-Type: application/json' \
-d "{\"text\":\"Deployment of $PM2_APP_NAME completed\"}"
# Clear application cache
cd "$RELEASE_PATH"
npm run cache:clearShipNode does not sync your .env file automatically (for security). Upload it once:
scp .env root@server:/var/www/myapp/shared/.env
# Or use the env command
shipnode envManage server users with users.yml:
users:
- username: alice
email: alice@company.com
password: "$6$rounds=5000$..." # generate with: shipnode mkpasswd
- username: bob
email: bob@company.com
authorized_key: "ssh-ed25519 AAAAC3... bob@laptop"
sudo: trueshipnode user sync # create users on server
shipnode user list # show all users
shipnode user remove bob # revoke access
shipnode mkpasswd # generate password hashshipnode hardenInteractive wizard to:
- Disable SSH password authentication
- Disable root SSH login
- Change SSH port
- Enable UFW firewall (22, 80, 443 only)
- Install and configure fail2ban
shipnode doctor --securityNon-destructive check of: SSH config, firewall status, fail2ban, file permissions.
# Generate workflow file
shipnode ci github
# Sync secrets (SSH_HOST, SSH_USER, SSH_PORT, SSH_PRIVATE_KEY)
shipnode ci env-sync --all
# Push and you're done
git add .github/workflows/deploy.yml
git commit -m "Add deployment workflow"
git push# Setup
shipnode init # Interactive config wizard
shipnode init --non-interactive # Non-interactive (for CI/CD)
shipnode init --template express # Use framework preset
shipnode init --print # Print config without writing
shipnode setup # Install Node.js, PM2, Caddy on server
# Deploy
shipnode deploy # Deploy your app
shipnode deploy --skip-build # Skip build step
shipnode deploy --dry-run # Preview without deploying
# Monitor
shipnode status # Rich status dashboard
shipnode logs # Stream live PM2 logs
shipnode metrics # Real-time CPU/memory monitor
shipnode releases # List all releases with history
# Manage
shipnode rollback # Rollback to previous release
shipnode rollback 2 # Rollback 2 releases back
shipnode restart # Restart app (graceful)
shipnode stop # Stop app
shipnode unlock # Clear stuck deployment lock
# Customize
shipnode eject # Eject PM2 + Caddy templates
shipnode eject pm2 # Eject only PM2 template
shipnode eject caddy # Eject only Caddy template
shipnode config # Show resolved config values
shipnode config validate # Validate config without deploying
# Environment
shipnode env # Upload .env to server
# Diagnostics
shipnode doctor # Pre-flight checks
shipnode doctor --security # Security audit
shipnode harden # Server hardening wizard
# CI/CD
shipnode ci github # Generate GitHub Actions workflow
shipnode ci env-sync # Sync config to GitHub secrets
shipnode ci env-sync --all # Sync config + .env to secrets
# User Management
shipnode user sync # Provision users from users.yml
shipnode user list # List provisioned users
shipnode user remove <user> # Revoke user access
shipnode mkpasswd # Generate password hash
# Multi-environment
shipnode deploy --config shipnode.staging.conf # Custom config
shipnode deploy --profile staging # Shorthand: shipnode.staging.conf# === Required ===
APP_TYPE=backend # "backend" or "frontend"
SSH_USER=root # SSH user for connecting to server
SSH_HOST=123.45.67.89 # Server IP or hostname
REMOTE_PATH=/var/www/app # Where your app lives on the server
# === Optional ===
SSH_PORT=22 # SSH port (default: 22)
NODE_VERSION=lts # Node.js version for setup (default: lts)
DOMAIN=myapp.com # Domain for automatic HTTPS via Caddy
PKG_MANAGER= # Override auto-detection (npm, yarn, pnpm, bun)
# === Backend-specific (required if APP_TYPE=backend) ===
PM2_APP_NAME=myapp # PM2 process name
BACKEND_PORT=3000 # Port your app listens on
# === Zero-downtime deployment ===
ZERO_DOWNTIME=true # Enable atomic deployments (default: true)
KEEP_RELEASES=5 # How many old releases to keep (default: 5)
# === Health checks (backend only) ===
HEALTH_CHECK_ENABLED=true # Enable health checks (default: true)
HEALTH_CHECK_PATH=/health # Endpoint to check (default: /health)
HEALTH_CHECK_TIMEOUT=30 # Seconds per attempt (default: 30)
HEALTH_CHECK_RETRIES=3 # Attempts before rollback (default: 3)
# === Hooks ===
# PRE_DEPLOY_SCRIPT=.shipnode/pre-deploy.sh
# POST_DEPLOY_SCRIPT=.shipnode/post-deploy.sh
# === Database and Redis setup ===
DB_SETUP_ENABLED=false # Set true to install/configure DB during setup
DB_TYPE=postgresql # postgresql, mysql, or sqlite
DB_NAME=myapp_db # PostgreSQL/MySQL database to create
DB_USER=myapp_user # PostgreSQL/MySQL user to create
DB_PASSWORD=${DB_PASSWORD:-} # PostgreSQL/MySQL password from env or .env
DB_SQLITE_PATH= # Optional; defaults to $REMOTE_PATH/shared/database.sqlite
REDIS_SETUP_ENABLED=false # Set true to install/configure Redis on localhostAuto-detected from your package.json:
| Framework | Type | Port | Health Check |
|---|---|---|---|
| Express | Backend | 3000 | /health |
| NestJS | Backend | 3000 | /api/health |
| Fastify | Backend | 3000 | /health |
| Koa | Backend | 3000 | /health |
| Hono | Backend | 3000 | /health |
| AdonisJS | Backend | 3333 | /health |
| Next.js | Backend (SSR) | 3000 | /api/health |
| Nuxt | Backend (SSR) | 3000 | /api/health |
| Remix | Backend | 3000 | /healthcheck |
| Astro | Backend/Frontend | 4321 | /api/health |
| React | Frontend | - | - |
| Vue | Frontend | - | - |
| Svelte | Frontend | - | - |
| Angular | Frontend | - | - |
| SolidJS | Frontend | - | - |
Auto-detected from lockfiles:
| Lockfile | Package Manager |
|---|---|
bun.lockb or bun.lock |
bun |
pnpm-lock.yaml |
pnpm |
yarn.lock |
yarn |
| (none) | npm |
Override in shipnode.conf: PKG_MANAGER=bun
These are excluded from rsync by default:
node_modules/- rebuilt on the server.env,.env.*- managed separately viashipnode env.git/- not needed in productionshipnode.conf,shipnode.*.conf- contains server credentials.shipnode/- local hooks and templates*.log- log files
Customize with .shipnodeignore (like .gitignore).
| Problem | Solution |
|---|---|
| Cannot connect to server | ssh -p 22 root@your-server to test |
| PM2 not found | shipnode setup to install |
| Build failed | Check package.json has a build script |
| Port already in use | Change BACKEND_PORT or kill the process |
| Health check fails | Verify /health endpoint: ssh root@server "curl localhost:3000/health" |
| Deployment lock stuck | shipnode unlock |
| Gum installation fails | Interactive commands try to install Gum locally; install manually from https://github.com/charmbracelet/gum |
| Framework not detected | Install jq, or select manually in wizard |
| Feature | ShipNode | PM2 Deploy | Capistrano | Kamal |
|---|---|---|---|---|
| Language | Bash | JS | Ruby | Ruby |
| Config files | 1 | 1 | Multiple | 1 |
| Zero-downtime | Built-in | Manual | Built-in | Built-in |
| Auto rollback | Yes | No | No | No |
| HTTPS | Automatic | Manual | Manual | Automatic |
| Frontend + Backend | Both | Backend | Backend | Both |
| Custom templates | Yes (eject) | Manual | Templates | No |
| Dependencies | None | Node.js | Ruby | Ruby + Docker |
For detailed guides and reference documentation, see the docs/ folder:
- Getting Started - Installation, quick start, first deploy
- Guides - Deployment, configuration, hooks, security, CI/CD
- Reference - Commands, frameworks, troubleshooting
- Examples - Express, NestJS, Next.js, React examples
- Advanced - Architecture, contributing, distribution
shipnode/
├── shipnode # Main entry point
├── lib/
│ ├── core.sh # Logging, template rendering
│ ├── pkg-manager.sh # Package manager detection
│ ├── release.sh # Zero-downtime release management
│ ├── framework.sh # Framework auto-detection
│ ├── validation.sh # Input validation
│ ├── prompts.sh # Interactive UI (Gum)
│ └── commands/
│ ├── init.sh # shipnode init
│ ├── setup.sh # shipnode setup
│ ├── deploy.sh # shipnode deploy
│ ├── status.sh # shipnode status (dashboard)
│ ├── rollback.sh # shipnode rollback
│ ├── eject.sh # shipnode eject (templates)
│ ├── metrics.sh # shipnode metrics
│ ├── config-cmd.sh # shipnode config
│ ├── doctor.sh # shipnode doctor
│ ├── harden.sh # shipnode harden
│ ├── ci.sh # shipnode ci
│ └── ...
├── templates/
│ ├── ecosystem.config.cjs.tmpl # PM2 template (for eject)
│ ├── Caddyfile.backend.tmpl # Caddy backend template (for eject)
│ ├── Caddyfile.frontend.tmpl # Caddy frontend template (for eject)
│ ├── pre-deploy.sh.template # Pre-deploy hook template
│ └── post-deploy.sh.template # Post-deploy hook template
├── examples/
│ ├── express-api/
│ ├── nestjs-api/
│ ├── nextjs-app/
│ └── react-router-app/
└── build.sh # Bundle into single file
See Architecture for module documentation.
ShipNode is intentionally simple. Contributions welcome for bug fixes and small improvements.
MIT