{"id":48581399,"url":"https://github.com/devalade/shipnode","last_synced_at":"2026-04-08T17:02:26.380Z","repository":{"id":334460812,"uuid":"1140890109","full_name":"devalade/shipnode","owner":"devalade","description":"Deploys your node app in a second","archived":false,"fork":false,"pushed_at":"2026-04-08T15:53:41.000Z","size":350,"stargazers_count":15,"open_issues_count":2,"forks_count":5,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-08T16:26:15.890Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/devalade.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-01-23T22:17:20.000Z","updated_at":"2026-04-08T15:54:49.000Z","dependencies_parsed_at":null,"dependency_job_id":"1996a059-3c90-4801-b39e-8f4ec1ae4f38","html_url":"https://github.com/devalade/shipnode","commit_stats":null,"previous_names":["devalade/shipnode"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/devalade/shipnode","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devalade%2Fshipnode","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devalade%2Fshipnode/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devalade%2Fshipnode/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devalade%2Fshipnode/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/devalade","download_url":"https://codeload.github.com/devalade/shipnode/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devalade%2Fshipnode/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31564915,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-08T14:31:17.711Z","status":"ssl_error","status_checked_at":"2026-04-08T14:31:17.202Z","response_time":54,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-08T17:02:25.637Z","updated_at":"2026-04-08T17:02:26.371Z","avatar_url":"https://github.com/devalade.png","language":"Shell","readme":"# ShipNode\n\nDeploy Node.js apps to your own server with a single command. No Kubernetes, no Docker, no vendor lock-in.\n\n```\nshipnode init \u0026\u0026 shipnode deploy\n```\n\n## How It Works\n\nShipNode is a CLI tool that deploys your Node.js backend or static frontend to any Ubuntu/Debian server over SSH. It handles everything: building, syncing files, process management, reverse proxying, and HTTPS.\n\n```\nYour Laptop                    Your Server\n─────────────                  ─────────────\nshipnode deploy  ───rsync──▶   /var/www/myapp/\n                                 ├── current/       ← active release (symlink)\n                                 ├── releases/      ← timestamped versions\n                                 ├── shared/.env    ← persistent config\n                                 └── PM2 + Caddy    ← process + HTTPS\n```\n\n### What gets installed on your server\n\nOne command (`shipnode setup`) installs everything:\n\n- **Node.js** - LTS version via NodeSource\n- **PM2** - Process manager (auto-restart, crash recovery)\n- **Caddy** - Web server with automatic HTTPS (Let's Encrypt)\n\n### What happens on every deploy\n\n**Backend (Express, NestJS, Next.js, AdonisJS, etc.):**\n\n1. Syncs your code to the server via rsync (excludes `node_modules`, `.env`, `.git`)\n2. Installs dependencies and builds on the server\n3. Creates a timestamped release directory\n4. Atomically switches a `current` symlink to the new release (zero downtime)\n5. Reloads PM2 gracefully\n6. Runs a health check against your `/health` endpoint\n7. If the health check fails, automatically rolls back to the previous release\n\n**Frontend (React, Vue, Svelte, etc.):**\n\n1. Builds your app locally\n2. Syncs the build output (`dist/`, `build/`, or `public/`) to the server\n3. Atomically switches the symlink\n4. Caddy serves the static files with SPA routing\n\n### What ShipNode manages for you\n\n| Component | Backend | Frontend |\n|-----------|---------|----------|\n| Process management | PM2 (auto-restart, crash recovery) | N/A (static files) |\n| Web server | Caddy reverse proxy | Caddy static file server |\n| HTTPS | Automatic via Caddy/Let's Encrypt | Automatic via Caddy/Let's Encrypt |\n| Environments | `.env` symlinked to each release | N/A |\n| Rollbacks | One command: `shipnode rollback` | One command: `shipnode rollback` |\n\n## Features\n\n- **Zero-downtime deployments** - Atomic symlink switching, never serve a broken build\n- **Automatic rollback** - Health check fails? Instantly reverts to the last working release\n- **Configurable templates** - Eject PM2 and Caddy configs for full customization\n- **Rich status dashboard** - See uptime, CPU, memory, release history at a glance\n- **Deployment tracking** - Every deploy records duration, git commit, health check timing\n- **Security hardening** - One-command firewall, SSH hardening, fail2ban setup\n- **CI/CD ready** - Generate GitHub Actions workflows with `shipnode ci github`\n- **User provisioning** - Manage SSH users with `users.yml`\n- **Pre/post deploy hooks** - Run migrations, clear caches, send notifications\n- **Auto-detection** - Frameworks (Express, NestJS, Next.js, etc.) and package managers (npm, yarn, pnpm, bun)\n- **Multi-environment** - Deploy to staging, production with `--config` or `--profile`\n- **Zero dependencies** - Pure bash, runs anywhere\n\n## Installation\n\n```bash\ncurl -fsSL https://github.com/devalade/shipnode/releases/latest/download/shipnode-installer.sh | bash\n```\n\nOr clone and run directly:\n\n```bash\ngit clone https://github.com/devalade/shipnode.git\ncd shipnode\n./shipnode help\n```\n\nSee [INSTALL.md](INSTALL.md) for details. Uninstall: `rm -rf ~/.shipnode`\n\n## Quick Start\n\n### 1. Init\n\n```bash\ncd /path/to/your/project\nshipnode init\n```\n\nThe interactive wizard auto-detects your framework, suggests defaults, and creates `shipnode.conf`:\n\n```\n╔════════════════════════════════════╗\n║  ShipNode Interactive Setup        ║\n╚════════════════════════════════════╝\n\n→ Detected framework: Express\n→ Suggested app type: backend\n\nApplication type:\n  1) Backend (Node.js API with PM2)\n  2) Frontend (Static site)\n\nChoose [1-2] (detected: backend): \nSSH user [root]: \nSSH host (IP or hostname): 203.0.113.10\nSSH port [22]: \nRemote deployment path [/var/www/myapp]: \nPM2 process name [myapp]: \nApplication port [3000]: \nDomain (optional): api.myapp.com\n\n════════════════════════════════════\nConfiguration Summary\n════════════════════════════════════\nApp Type:      backend\nSSH:           root@203.0.113.10:22\nRemote Path:   /var/www/myapp\nPM2 Name:      myapp\nBackend Port:  3000\nDomain:        api.myapp.com\nZero-downtime: true\nHealth Checks: /health (30s timeout, 3 retries)\n════════════════════════════════════\n\nCreate shipnode.conf with these settings? (Y/n): \n```\n\nNon-interactive mode for scripts/CI:\n\n```bash\nshipnode init --non-interactive\n```\n\n### 2. Setup server (first time only)\n\n```bash\nshipnode setup\n```\n\nInstalls Node.js, PM2, and Caddy on your server.\n\n### 3. Deploy\n\n```bash\nshipnode deploy\n```\n\nYour app is live. That's it.\n\n### 4. Check status\n\n```bash\nshipnode status\n```\n\n```\n═══════════════════════════════════════\n  Application Status\n═══════════════════════════════════════\n\n  App:        myapp (backend)\n  URL:        https://api.myapp.com\n  Server:     root@203.0.113.10:22\n\n  PM2:\n    Status:    ● online\n    Uptime:    2d 14h 32m\n    Restarts:  0\n    Instances: 1\n    CPU:       3.2%\n    Memory:    128MB\n    Port:      3000\n\n  Release:\n    Current:   20260408143022 (2 hours ago)\n    Previous:  20260408120000\n\n  Disk:\n    Total:     48GB\n    Used:      12GB (25%)\n    Releases:  1.2GB (3 releases)\n\n═══════════════════════════════════════\n```\n\n## Commands\n\n```bash\n# Setup\nshipnode init                          # Interactive config wizard\nshipnode init --non-interactive        # Non-interactive (for CI/CD)\nshipnode init --template express       # Use framework preset\nshipnode init --print                  # Print config without writing\nshipnode setup                         # Install Node.js, PM2, Caddy on server\n\n# Deploy\nshipnode deploy                        # Deploy your app\nshipnode deploy --skip-build           # Skip build step\nshipnode deploy --dry-run              # Preview without deploying\n\n# Monitor\nshipnode status                        # Rich status dashboard\nshipnode logs                          # Stream live PM2 logs\nshipnode metrics                       # Real-time CPU/memory monitor\nshipnode releases                      # List all releases with history\n\n# Manage\nshipnode rollback                      # Rollback to previous release\nshipnode rollback 2                    # Rollback 2 releases back\nshipnode restart                       # Restart app (graceful)\nshipnode stop                          # Stop app\nshipnode unlock                        # Clear stuck deployment lock\n\n# Customize\nshipnode eject                         # Eject PM2 + Caddy templates\nshipnode eject pm2                     # Eject only PM2 template\nshipnode eject caddy                   # Eject only Caddy template\nshipnode config                        # Show resolved config values\nshipnode config validate               # Validate config without deploying\n\n# Environment\nshipnode env                           # Upload .env to server\n\n# Diagnostics\nshipnode doctor                        # Pre-flight checks\nshipnode doctor --security             # Security audit\nshipnode harden                        # Server hardening wizard\n\n# CI/CD\nshipnode ci github                     # Generate GitHub Actions workflow\nshipnode ci env-sync                   # Sync config to GitHub secrets\nshipnode ci env-sync --all             # Sync config + .env to secrets\n\n# User Management\nshipnode user sync                     # Provision users from users.yml\nshipnode user list                     # List provisioned users\nshipnode user remove \u003cuser\u003e            # Revoke user access\nshipnode mkpasswd                      # Generate password hash\n\n# Multi-environment\nshipnode deploy --config shipnode.staging.conf   # Custom config\nshipnode deploy --profile staging                # Shorthand: shipnode.staging.conf\n```\n\n## Zero-Downtime Deployments\n\nShipNode uses the same pattern as Capistrano: timestamped releases with an atomic symlink switch.\n\n### Directory structure on your server\n\n```\n/var/www/myapp/\n├── current -\u003e releases/20260408143022/   ← always points to active release\n├── releases/\n│   ├── 20260408120000/                   ← previous release (for rollback)\n│   └── 20260408143022/                   ← current release\n├── shared/\n│   ├── .env                              ← persistent env vars (symlinked into each release)\n│   └── ecosystem.config.cjs              ← PM2 config\n└── .shipnode/\n    ├── releases.json                     ← deployment history with metadata\n    └── deploy.lock                       ← prevents concurrent deploys\n```\n\n### Deployment lifecycle\n\n```\n  1. Acquire lock\n  2. Create release directory    releases/20260408143022/\n  3. rsync files                 your laptop → server\n  4. Link shared .env            shared/.env → releases/.../ .env\n  5. Install dependencies        npm install\n  6. Build (if needed)           npm run build\n  7. Run pre-deploy hook         migrations, cache warm, etc.\n  8. Atomic symlink switch       current → releases/20260408143022/\n  9. Reload PM2                  graceful reload, zero downtime\n 10. Health check                GET localhost:3000/health (3 retries)\n 11. Record release              timestamp, git commit, duration, health data\n 12. Run post-deploy hook        notifications, cache clear, etc.\n 13. Cleanup old releases        keep last 5 by default\n 14. Release lock\n```\n\nIf step 10 fails, ShipNode immediately:\n- Switches symlink back to the previous release\n- Reloads PM2\n- Records the failed deployment\n\n### Rollback\n\n```bash\nshipnode rollback       # previous release\nshipnode rollback 2     # 2 releases back\nshipnode releases       # see all releases first\n```\n\n### Deployment history\n\nEvery deployment is recorded in `.shipnode/releases.json` with rich metadata:\n\n```json\n{\n  \"timestamp\": \"20260408143022\",\n  \"date\": \"2026-04-08T14:30:22Z\",\n  \"status\": \"success\",\n  \"duration_seconds\": 45,\n  \"commit\": \"abc1234\",\n  \"previous_release\": \"20260408120000\",\n  \"health_check\": {\n    \"passed\": true,\n    \"attempts\": 1,\n    \"response_time_ms\": 23\n  }\n}\n```\n\n### Health checks\n\nAdd a `/health` endpoint to your backend:\n\n```javascript\napp.get('/health', (req, res) =\u003e {\n  res.status(200).json({ status: 'ok' });\n});\n```\n\nConfigure in `shipnode.conf`:\n\n```bash\nHEALTH_CHECK_ENABLED=true       # default: true\nHEALTH_CHECK_PATH=/health       # default: /health\nHEALTH_CHECK_TIMEOUT=30         # seconds per attempt (default: 30)\nHEALTH_CHECK_RETRIES=3          # attempts before rollback (default: 3)\n```\n\n## Customizing Templates\n\nShipNode generates PM2 and Caddy configs automatically. For most projects, the defaults work great. But when you need cluster mode, custom headers, rate limiting, or memory limits, you can **eject** the configs and customize them.\n\n### `shipnode eject`\n\n```bash\nshipnode eject            # eject PM2 + Caddy templates\nshipnode eject pm2        # eject only PM2 template\nshipnode eject caddy      # eject only Caddy template\n```\n\nThis creates editable templates in `.shipnode/templates/`:\n\n```\n.shipnode/templates/\n├── ecosystem.config.cjs    ← customize PM2: cluster mode, memory limits, env vars\n└── Caddyfile.caddy         ← customize Caddy: headers, TLS, rate limiting, caching\n```\n\nEjected templates are **preserved across deploys**. ShipNode will use your custom versions instead of the defaults.\n\n### Template variables\n\nTemplates use `{{VAR}}` placeholders that ShipNode replaces on every deploy:\n\n| Variable | Description |\n|----------|-------------|\n| `{{APP_NAME}}` | PM2 process name |\n| `{{INTERPRETER}}` | Package manager (npm, yarn, pnpm, bun) |\n| `{{REMOTE_PATH}}` | Deployment path on server |\n| `{{BACKEND_PORT}}` | Application port |\n| `{{DOMAIN}}` | Your domain name |\n| `{{SERVE_PATH}}` | Path to static files (frontend) |\n\n### Custom PM2 config example\n\nAfter `shipnode eject pm2`, edit `.shipnode/templates/ecosystem.config.cjs`:\n\n```javascript\nmodule.exports = {\n  apps: [{\n    name: \"{{APP_NAME}}\",\n    script: \"{{INTERPRETER}}\",\n    args: \"start\",\n    cwd: \"{{REMOTE_PATH}}/current\",\n    instances: \"max\",              // use all CPU cores\n    exec_mode: \"cluster\",          // enable cluster mode\n    max_memory_restart: \"1G\",      // restart if memory exceeds 1GB\n    env: {\n      NODE_ENV: \"production\",\n      PORT: {{BACKEND_PORT}}\n    }\n  }]\n};\n```\n\n### Custom Caddy config example\n\nAfter `shipnode eject caddy`, edit `.shipnode/templates/Caddyfile.caddy`:\n\n```\n{{DOMAIN}} {\n    reverse_proxy localhost:{{BACKEND_PORT}}\n    encode gzip\n\n    # Custom rate limiting\n    rate_limit {\n        zone dynamic_zone {\n            key    {remote_host}\n            events 100\n            window 1s\n        }\n    }\n\n    # Custom headers\n    header {\n        X-Content-Type-Options nosniff\n        X-Frame-Options DENY\n        Strict-Transport-Security \"max-age=31536000; includeSubDomains\"\n    }\n\n    log {\n        output file /var/log/caddy/{{APP_NAME}}.log\n        format json\n    }\n}\n```\n\n### How template resolution works\n\nShipNode looks for templates in this order:\n\n1. `.shipnode/templates/ecosystem.config.cjs` (ejected, user-customized)\n2. `ecosystem.config.cjs` in project root (user-provided)\n3. Built-in defaults (auto-generated, used when no custom template exists)\n\nTo reset to defaults, just delete the ejected files:\n\n```bash\nrm .shipnode/templates/ecosystem.config.cjs\nrm .shipnode/templates/Caddyfile.caddy\n```\n\n## What gets excluded from deployment\n\nShipNode doesn't sync everything to your server. By default, these are excluded:\n\n- `node_modules/` - rebuilt on the server\n- `.env`, `.env.*` - managed separately via `shipnode env`\n- `.git/` - not needed in production\n- `shipnode.conf`, `shipnode.*.conf` - contains server credentials\n- `.shipnode/` - local hooks and templates\n- `*.log` - log files\n\n**Customize with `.shipnodeignore`:**\n\n```bash\nshipnode eject        # generates .shipnodeignore with sensible defaults\n```\n\nEdit it like `.gitignore` - one pattern per line:\n\n```\n# Exclude test files\ntest/\ncoverage/\n*.test.ts\n\n# But keep this specific file\n! keep-this.txt\n\n# Exclude Docker files\nDockerfile\ndocker-compose.yml\n```\n\n## Pre/Post Deploy Hooks\n\nShipNode auto-generates hook scripts in `.shipnode/` during `shipnode init`. These run on your server during deployment.\n\n### Pre-deploy hook (`.shipnode/pre-deploy.sh`)\n\nRuns **before** the new release goes live. If it fails, the deployment aborts and rolls back.\n\nUse for: database migrations, Prisma generate, cache warming.\n\n```bash\n#!/bin/bash\n# Auto-generated by shipnode init\n# Available: RELEASE_PATH, REMOTE_PATH, PM2_APP_NAME, BACKEND_PORT, SHARED_ENV_PATH\n\nset -e\nsource \"$SHARED_ENV_PATH\"  # load .env variables\ncd \"$RELEASE_PATH\"\n\n# Prisma migrations (auto-detected by shipnode init)\nnpx prisma generate\nnpx prisma migrate deploy\n```\n\n### Post-deploy hook (`.shipnode/post-deploy.sh`)\n\nRuns **after** the deployment succeeds. If it fails, the deployment is still considered successful.\n\nUse for: notifications, cache clearing, cleanup.\n\n```bash\n#!/bin/bash\n# Auto-generated by shipnode init\n\nset -e\nsource \"$SHARED_ENV_PATH\"\n\n# Send Slack notification\ncurl -X POST \"$SLACK_WEBHOOK\" \\\n  -H 'Content-Type: application/json' \\\n  -d \"{\\\"text\\\":\\\"Deployment of $PM2_APP_NAME completed\\\"}\"\n\n# Clear application cache\ncd \"$RELEASE_PATH\"\nnpm run cache:clear\n```\n\n## Configuration\n\n### `shipnode.conf` reference\n\n```bash\n# === Required ===\nAPP_TYPE=backend             # \"backend\" or \"frontend\"\nSSH_USER=root                # SSH user for connecting to server\nSSH_HOST=123.45.67.89        # Server IP or hostname\nREMOTE_PATH=/var/www/app     # Where your app lives on the server\n\n# === Optional ===\nSSH_PORT=22                  # SSH port (default: 22)\nNODE_VERSION=lts             # Node.js version for setup (default: lts)\nDOMAIN=myapp.com             # Domain for automatic HTTPS via Caddy\nPKG_MANAGER=                 # Override auto-detection (npm, yarn, pnpm, bun)\n\n# === Backend-specific (required if APP_TYPE=backend) ===\nPM2_APP_NAME=myapp           # PM2 process name\nBACKEND_PORT=3000            # Port your app listens on\n\n# === Zero-downtime deployment ===\nZERO_DOWNTIME=true           # Enable atomic deployments (default: true)\nKEEP_RELEASES=5              # How many old releases to keep (default: 5)\n\n# === Health checks (backend only) ===\nHEALTH_CHECK_ENABLED=true    # Enable health checks (default: true)\nHEALTH_CHECK_PATH=/health    # Endpoint to check (default: /health)\nHEALTH_CHECK_TIMEOUT=30      # Seconds per attempt (default: 30)\nHEALTH_CHECK_RETRIES=3       # Attempts before rollback (default: 3)\n\n# === Hooks ===\n# ShipNode uses .shipnode/pre-deploy.sh and .shipnode/post-deploy.sh by default.\n# Override paths here if needed:\n# PRE_DEPLOY_SCRIPT=.shipnode/pre-deploy.sh\n# POST_DEPLOY_SCRIPT=.shipnode/post-deploy.sh\n```\n\n### Multi-environment\n\nUse different config files for different environments:\n\n```bash\nshipnode deploy                              # uses shipnode.conf (production)\nshipnode deploy --profile staging            # uses shipnode.staging.conf\nshipnode deploy --config shipnode.prod.conf  # uses custom config file\n```\n\n## Package Manager Support\n\nShipNode auto-detects your package manager from lockfiles:\n\n| Lockfile | Package Manager |\n|----------|----------------|\n| `bun.lockb` or `bun.lock` | bun |\n| `pnpm-lock.yaml` | pnpm |\n| `yarn.lock` | yarn |\n| (none) | npm |\n\nOverride in `shipnode.conf`: `PKG_MANAGER=bun`\n\n## Backend Examples\n\n### Express API\n\n```bash\n# Project structure\nmyapi/\n├── src/index.js\n├── package.json\n├── .env\n└── shipnode.conf\n\n# shipnode.conf\nAPP_TYPE=backend\nSSH_USER=root\nSSH_HOST=123.45.67.89\nREMOTE_PATH=/var/www/myapi\nPM2_APP_NAME=myapi\nBACKEND_PORT=3000\nDOMAIN=api.myapp.com\n\n# Deploy\nshipnode deploy\n# → Live at https://api.myapp.com\n```\n\n### NestJS with Prisma\n\n```bash\n# shipnode init auto-detects NestJS and generates pre-deploy hook with:\n# npx prisma generate \u0026\u0026 npx prisma migrate deploy\n\nshipnode init     # wizard detects NestJS + Prisma\nshipnode deploy   # builds, migrates, deploys\n```\n\n### Next.js (SSR)\n\n```bash\n# Next.js runs as a backend (Node.js server)\n# Uses output: 'standalone' in next.config.js for optimized builds\n\nAPP_TYPE=backend\nPM2_APP_NAME=mywebapp\nBACKEND_PORT=3000\nDOMAIN=myapp.com\n```\n\n## Frontend Examples\n\n### React / Vue / Svelte SPA\n\n```bash\nAPP_TYPE=frontend\nSSH_USER=root\nSSH_HOST=123.45.67.89\nREMOTE_PATH=/var/www/myapp\nDOMAIN=myapp.com\n\n# ShipNode builds locally (npm run build), syncs dist/, Caddy serves it\nshipnode deploy\n# → Live at https://myapp.com with SPA routing + aggressive asset caching\n```\n\n### Skip build\n\n```bash\nshipnode deploy --skip-build    # deploy pre-built files\n```\n\n## Environment Variables\n\nShipNode does **not** sync your `.env` file automatically (for security). Upload it once:\n\n```bash\n# Zero-downtime: .env lives in shared/ and is symlinked into each release\nscp .env root@server:/var/www/myapp/shared/.env\n\n# Or use the env command\nshipnode env\n```\n\n## Observability\n\n### `shipnode status` - Application dashboard\n\nShows everything at a glance: PM2 status, uptime, CPU, memory, current release, disk usage.\n\n```bash\nshipnode status\n```\n\n### `shipnode metrics` - Real-time monitoring\n\nOpens the PM2 monitoring dashboard over SSH. Shows live CPU, memory, and log streams.\n\n```bash\nshipnode metrics\n# Press Ctrl+C to exit\n```\n\n### `shipnode logs` - Live log stream\n\n```bash\nshipnode logs          # stream PM2 logs\nshipnode restart       # restart app\nshipnode stop          # stop app\n```\n\n### `shipnode releases` - Release history\n\nLists all deployments with timestamps and status.\n\n```bash\nshipnode releases\n```\n\n### Deployment metadata\n\nEvery deployment records:\n- **Duration** - how long the deploy took\n- **Git commit** - which commit was deployed (from `git rev-parse --short HEAD`)\n- **Health check** - pass/fail, attempts, response time in milliseconds\n- **Previous release** - which version it replaced\n\n## Security\n\n### Server hardening\n\n```bash\nshipnode harden\n```\n\nInteractive wizard to:\n- Disable SSH password authentication\n- Disable root SSH login\n- Change SSH port\n- Enable UFW firewall (22, 80, 443 only)\n- Install and configure fail2ban\n\nAll changes are **opt-in** - you choose what to apply.\n\n### Security audit\n\n```bash\nshipnode doctor --security\n```\n\nNon-destructive check of: SSH config, firewall status, fail2ban, file permissions.\n\n### Pre-flight checks\n\n```bash\nshipnode doctor\n```\n\nValidates your entire setup: local config, SSH connectivity, remote Node.js/PM2/Caddy, disk space.\n\n## CI/CD Integration\n\n### GitHub Actions\n\n```bash\n# Generate workflow file\nshipnode ci github\n\n# Sync secrets (SSH_HOST, SSH_USER, SSH_PORT, SSH_PRIVATE_KEY)\nshipnode ci env-sync --all\n\n# Push and you're done\ngit add .github/workflows/deploy.yml\ngit commit -m \"Add deployment workflow\"\ngit push\n```\n\n## User Provisioning\n\nManage server users with `users.yml`:\n\n```yaml\nusers:\n  - username: alice\n    email: alice@company.com\n    password: \"$6$rounds=5000$...\"     # generate with: shipnode mkpasswd\n\n  - username: bob\n    email: bob@company.com\n    authorized_key: \"ssh-ed25519 AAAAC3... bob@laptop\"\n    sudo: true\n```\n\n```bash\nshipnode user sync           # create users on server\nshipnode user list           # show all users\nshipnode user remove bob     # revoke access\nshipnode mkpasswd            # generate password hash\n```\n\nAll users get:\n- SSH or password authentication\n- Deployment directory access via ACLs\n- PM2 management via passwordless sudo\n\n## Supported Frameworks\n\nAuto-detected from your `package.json`:\n\n| Framework | Type | Port | Health Check |\n|-----------|------|------|-------------|\n| Express | Backend | 3000 | `/health` |\n| NestJS | Backend | 3000 | `/api/health` |\n| Fastify | Backend | 3000 | `/health` |\n| Koa | Backend | 3000 | `/health` |\n| Hono | Backend | 3000 | `/health` |\n| AdonisJS | Backend | 3333 | `/health` |\n| Next.js | Backend (SSR) | 3000 | `/api/health` |\n| Nuxt | Backend (SSR) | 3000 | `/api/health` |\n| Remix | Backend | 3000 | `/healthcheck` |\n| Astro | Backend/Frontend | 4321 | `/api/health` |\n| React | Frontend | - | - |\n| Vue | Frontend | - | - |\n| Svelte | Frontend | - | - |\n| Angular | Frontend | - | - |\n| SolidJS | Frontend | - | - |\n\nUse `shipnode init --template \u003cname\u003e` to use a specific preset, or `shipnode init --list-templates` to see all options.\n\n## Troubleshooting\n\n| Problem | Solution |\n|---------|----------|\n| Cannot connect to server | `ssh -p 22 root@your-server` to test |\n| PM2 not found | `shipnode setup` to install |\n| Build failed | Check `package.json` has a `build` script |\n| Port already in use | Change `BACKEND_PORT` or kill the process |\n| Health check fails | Verify `/health` endpoint: `ssh root@server \"curl localhost:3000/health\"` |\n| Deployment lock stuck | `shipnode unlock` |\n| Gum installation fails | Install manually: `sudo apt install gum` |\n| Framework not detected | Install `jq`, or select manually in wizard |\n\n## Comparison\n\n| Feature | ShipNode | PM2 Deploy | Capistrano | Kamal |\n|---------|----------|------------|------------|-------|\n| Language | Bash | JS | Ruby | Ruby |\n| Config files | 1 | 1 | Multiple | 1 |\n| Zero-downtime | Built-in | Manual | Built-in | Built-in |\n| Auto rollback | Yes | No | No | No |\n| HTTPS | Automatic | Manual | Manual | Automatic |\n| Frontend + Backend | Both | Backend | Backend | Both |\n| Custom templates | Yes (eject) | Manual | Templates | No |\n| Dependencies | None | Node.js | Ruby | Ruby + Docker |\n\n## Project Structure\n\n```\nshipnode/\n├── shipnode                          # Main entry point\n├── lib/\n│   ├── core.sh                       # Logging, template rendering\n│   ├── pkg-manager.sh                # Package manager detection\n│   ├── release.sh                    # Zero-downtime release management\n│   ├── framework.sh                  # Framework auto-detection\n│   ├── validation.sh                 # Input validation\n│   ├── prompts.sh                    # Interactive UI (Gum)\n│   └── commands/\n│       ├── init.sh                   # shipnode init\n│       ├── setup.sh                  # shipnode setup\n│       ├── deploy.sh                 # shipnode deploy\n│       ├── status.sh                 # shipnode status (dashboard)\n│       ├── rollback.sh               # shipnode rollback\n│       ├── eject.sh                  # shipnode eject (templates)\n│       ├── metrics.sh                # shipnode metrics\n│       ├── config-cmd.sh             # shipnode config\n│       ├── doctor.sh                 # shipnode doctor\n│       ├── harden.sh                 # shipnode harden\n│       ├── ci.sh                     # shipnode ci\n│       └── ...\n├── templates/\n│   ├── ecosystem.config.cjs.tmpl     # PM2 template (for eject)\n│   ├── Caddyfile.backend.tmpl        # Caddy backend template (for eject)\n│   ├── Caddyfile.frontend.tmpl       # Caddy frontend template (for eject)\n│   ├── pre-deploy.sh.template        # Pre-deploy hook template\n│   └── post-deploy.sh.template       # Post-deploy hook template\n├── examples/\n│   ├── express-api/\n│   ├── nestjs-api/\n│   ├── nextjs-app/\n│   └── react-router-app/\n└── build.sh                          # Bundle into single file\n```\n\nSee [ARCHITECTURE.md](ARCHITECTURE.md) for module documentation.\n\n## Contributing\n\nShipNode is intentionally simple. Contributions welcome for bug fixes and small improvements.\n\n## License\n\nMIT\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevalade%2Fshipnode","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdevalade%2Fshipnode","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevalade%2Fshipnode/lists"}