{"id":48581399,"url":"https://github.com/devalade/shipnode","last_synced_at":"2026-05-16T23:03:39.695Z","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-05-16T23:03:39.688Z","avatar_url":"https://github.com/devalade.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"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```bash\nshipnode init \u0026\u0026 shipnode deploy\n```\n\n## How It Works\n\nShipNode 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.\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** (`shipnode setup`):\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- **Databases** - optional PostgreSQL, MySQL, SQLite, and Redis setup when enabled\n\n---\n\n## Getting Started\n\n### 1. Install\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 [Installation](docs/getting-started/installation.md) for details. Uninstall: `rm -rf ~/.shipnode`\n\n### Optional: Install the AI Deployment Skill\n\nShipNode 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.\n\nInstall it with the [`skills` CLI](https://skills.sh/docs):\n\n```bash\nnpx skills add devalade/shipnode\n```\n\nAfter installing, ask your agent to use `$shipnode` when you want help deploying a Node.js app with ShipNode.\n\n### 2. 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### 3. Setup Server\n\n```bash\nshipnode setup\n```\n\nInstalls Node.js, PM2, and Caddy on your server. Run once per server.\n\n### 4. Deploy\n\n```bash\nshipnode deploy\n```\n\nYour app is live. That's it.\n\n### 5. Check Status\n\n```bash\nshipnode status\n```\n\n---\n\n## Deploy a Backend\n\nDeploy Express, NestJS, Next.js, AdonisJS, or any Node.js backend.\n\n### Prerequisites\n\n- Add a `/health` endpoint to your app:\n\n```javascript\napp.get('/health', (req, res) =\u003e {\n  res.status(200).json({ status: 'ok' });\n});\n```\n\n### Deploy\n\n```bash\nshipnode deploy\n```\n\n**What happens:**\n\n1. Syncs code via rsync (excludes `node_modules`, `.env`, `.git`)\n2. Installs dependencies and builds on the server\n3. Creates a timestamped release directory\n4. Atomically switches `current` symlink to new release (zero downtime)\n5. Reloads PM2 gracefully\n6. Runs a health check against `/health`\n7. On failure: automatically rolls back to previous release\n\n### Health Check Configuration\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---\n\n## Deploy a Frontend\n\nDeploy React, Vue, Svelte, or any static SPA.\n\n### Configuration\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\n### Deploy\n\n```bash\nshipnode deploy\n```\n\n**What happens:**\n\n1. Builds your app locally (`npm run build`)\n2. Syncs the build output (`dist/`, `build/`, or `public/`) to the server\n3. Atomically switches the symlink\n4. Caddy serves static files with SPA routing and aggressive caching\n\nSkip the build step if files are pre-built:\n\n```bash\nshipnode deploy --skip-build\n```\n\n---\n\n## Rollback\n\nIf a deployment fails health checks or you need to revert:\n\n```bash\nshipnode rollback           # previous release\nshipnode rollback 2         # 2 releases back\nshipnode releases           # see all releases first\n```\n\n### Directory Structure\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\n│   └── ecosystem.config.cjs              ← PM2 config\n└── .shipnode/\n    ├── releases.json                     ← deployment history\n    └── deploy.lock                       ← prevents concurrent deploys\n```\n\n---\n\n## Multi-Environment Deployments\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---\n\n## Zero-Downtime Deployments\n\nShipNode uses timestamped releases with atomic symlink switching (same pattern as Capistrano).\n\n### Deployment Lifecycle\n\n```\n 1. Acquire lock\n 2. Create release directory    releases/20260408143022/\n 3. rsync files                 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\n10. Health check                GET localhost:3000/health (3 retries)\n11. Record release              timestamp, git commit, duration\n12. Run post-deploy hook        notifications, cache clear, etc.\n13. Cleanup old releases        keep last 5 by default\n14. Release lock\n```\n\nIf step 10 fails, ShipNode immediately reverts to the previous release.\n\n### Deployment History\n\nEvery deployment is recorded in `.shipnode/releases.json`:\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---\n\n## Customization\n\n### Eject Templates\n\nShipNode generates PM2 and Caddy configs automatically. Eject to customize:\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\n└── Caddyfile.caddy         ← customize Caddy: headers, TLS, rate limiting\n```\n\nEjected templates are **preserved across deploys**.\n\n### Template Variables\n\nTemplates use `{{VAR}}` placeholders replaced 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 Example\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 Example\n\n```\n{{DOMAIN}} {\n    reverse_proxy localhost:{{BACKEND_PORT}}\n    encode gzip\n\n    rate_limit {\n        zone dynamic_zone {\n            key    {remote_host}\n            events 100\n            window 1s\n        }\n    }\n\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### Reset to Defaults\n\n```bash\nrm .shipnode/templates/ecosystem.config.cjs\nrm .shipnode/templates/Caddyfile.caddy\n```\n\n---\n\n## Hooks\n\n### Pre-Deploy Hook\n\nRuns **before** the new release goes live. If it fails, deployment aborts and rolls back.\n\n```bash\n#!/bin/bash\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\n\nRuns **after** deployment succeeds. Failures don't affect deployment status.\n\n```bash\n#!/bin/bash\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---\n\n## Environment Variables\n\nShipNode does **not** sync your `.env` file automatically (for security). Upload it once:\n\n```bash\nscp .env root@server:/var/www/myapp/shared/.env\n\n# Or use the env command\nshipnode env\n```\n\n---\n\n## User Management\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\n---\n\n## Security\n\n### Server Hardening\n\n```bash\nshipnode harden\n```\n\nInteractive wizard to:\n\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\n### Security Audit\n\n```bash\nshipnode doctor --security\n```\n\nNon-destructive check of: SSH config, firewall status, fail2ban, file permissions.\n\n### Cloudflare Easy Mode\n\n```bash\nexport CLOUDFLARE_API_TOKEN=...\nshipnode cloudflare init\nshipnode cloudflare audit\n```\n\nUse Cloudflare Easy Mode when you want `DOMAIN` and `SSH_HOST` to be Cloudflare hostnames, with no origin IP committed to `shipnode.conf`:\n\n```bash\nDOMAIN=app.example.com\nSSH_HOST=ssh.example.com\nSSH_PROXY_MODE=cloudflare\nCLOUDFLARE_ENABLED=true\nCLOUDFLARE_ZONE=example.com\nCLOUDFLARE_LOCKDOWN_FIREWALL=true\nCLOUDFLARE_ACCESS_EMAILS=you@example.com\n```\n\n`cloudflare init` creates or reuses a Cloudflare Tunnel, routes app and SSH hostnames through it, configures Access SSH, installs `cloudflared` on the server, and can block direct inbound `22`, `80`, and `443`. Firewall lockdown is skipped until an Access policy exists, so set `CLOUDFLARE_ACCESS_EMAILS` for the easiest safe path. For first-time setup, set `SHIPNODE_BOOTSTRAP_SSH_HOST` temporarily if `ssh.example.com` is not reachable yet.\n\nFor production, use a Cloudflare Account API Token rather than a user-owned token. Full guide: [Cloudflare Easy Mode](./docs/guides/cloudflare.md)\n\n---\n\n## CI/CD Integration\n\n### GitHub Actions\n\n```bash\n# Generate workflow file\nshipnode ci github\n\n# Sync secrets (SHIPNODE_SSH_HOST, SHIPNODE_SSH_USER, SHIPNODE_SSH_PORT)\nshipnode ci env-sync --all\n\n# Set the deploy SSH key separately\ngh secret set SHIPNODE_SSH_KEY \u003c ~/.ssh/id_ed25519\n\n# Push and you're done\ngit add .github/workflows/deploy.yml\ngit commit -m \"Add deployment workflow\"\ngit push\n```\n\n---\n\n## Reference\n\n### All 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\nshipnode run \u003ccmd\u003e                     # Run a command in the app context\nshipnode run --tty \u003ccmd\u003e               # Force interactive TTY session\nshipnode run bash                      # Open interactive shell (auto-detected)\nshipnode run \"node -v\" --profile staging       # Use profile config/runtime\nshipnode run \"npm run migrate\" --config shipnode.prod.conf\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`shipnode run` executes inside `$REMOTE_PATH/current`, sources `$REMOTE_PATH/shared/.env`, and uses the Node.js runtime from `NODE_VERSION`. It also repairs execute permissions for package-declared binaries in `node_modules` before running the command, which prevents common `Permission denied` failures from package CLIs like Prisma, Vite, Next, tsx, or esbuild.\n\n### Configuration 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)\nSSH_PROXY_MODE=direct        # \"direct\" or \"cloudflare\"\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# === Cloudflare Easy Mode ===\n# CLOUDFLARE_ENABLED=true\n# CLOUDFLARE_ZONE=example.com\n# CLOUDFLARE_LOCKDOWN_FIREWALL=true\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# PRE_DEPLOY_SCRIPT=.shipnode/pre-deploy.sh\n# POST_DEPLOY_SCRIPT=.shipnode/post-deploy.sh\n\n# === Database and Redis setup ===\nDB_SETUP_ENABLED=false       # Set true to install/configure DB during setup\nDB_TYPE=postgresql           # postgresql, mysql, or sqlite\nDB_NAME=myapp_db             # PostgreSQL/MySQL database to create\nDB_USER=myapp_user           # PostgreSQL/MySQL user to create\nDB_PASSWORD=${DB_PASSWORD:-} # PostgreSQL/MySQL password from env or .env\nDB_SQLITE_PATH=              # Optional; defaults to $REMOTE_PATH/shared/database.sqlite\nREDIS_SETUP_ENABLED=false    # Set true to install/configure Redis on localhost\n\n# === Database backups to S3 ===\nDB_BACKUP_ENABLED=false      # Set true to configure scheduled backups\nDB_BACKUP_S3_BUCKET=         # S3 bucket for uploaded backup files\nDB_BACKUP_S3_PREFIX=myapp    # Optional path prefix inside the bucket\nDB_BACKUP_SCHEDULE=daily     # hourly, daily, weekly, or systemd OnCalendar\nDB_BACKUP_RETENTION_DAYS=14  # Local compressed backup retention\nDB_BACKUP_S3_ENDPOINT=       # Optional S3-compatible endpoint\nAWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}\nAWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}\nAWS_DEFAULT_REGION=eu-west-1\n```\n\n### Database Backups\n\nShipNode can install a small remote backup script and a systemd timer that dumps the configured PostgreSQL, MySQL, or SQLite database, compresses it, and uploads it to S3.\n\n```bash\nDB_BACKUP_ENABLED=true\nDB_BACKUP_S3_BUCKET=my-backups\nDB_BACKUP_S3_PREFIX=myapp/production\nDB_BACKUP_SCHEDULE=daily\nAWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}\nAWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}\nAWS_DEFAULT_REGION=eu-west-1\n```\n\nPut real AWS/S3 credentials in `.env`, upload them with `shipnode env`, then configure backups:\n\n```bash\nshipnode setup\nshipnode backup setup\nshipnode backup run\nshipnode backup status\n```\n\nUse `DB_BACKUP_S3_ENDPOINT` for S3-compatible storage such as Cloudflare R2, MinIO, or DigitalOcean Spaces. `DB_BACKUP_RETENTION_DAYS` controls local compressed files on the server; use your bucket lifecycle policy for S3 retention.\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\n### Package Manager Support\n\nAuto-detected 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### Excluded Files\n\nThese are excluded from rsync by default:\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\nCustomize with `.shipnodeignore` (like `.gitignore`).\n\n### Shared Assets and Uploads\n\nUse shared resources for files that must survive deploys and rollbacks, such as user uploads, generated media, cached documents, or runtime storage.\n\n```bash\n# shipnode.conf\nSHARED_DIRS=\"public/uploads storage\"\nSHARED_FILES=\"\"\n```\n\nDuring deployment, ShipNode stores those paths under `$REMOTE_PATH/shared/` and symlinks them into each release. For legacy frontend deploys, backing storage lives at `$REMOTE_PATH.shared/` so it is not exposed by the static file server. If a configured shared directory or file already exists in the first uploaded release, ShipNode promotes it before creating the symlink.\n\nKeep versioned images, fonts, and frontend assets in your app as usual. For large media libraries, prefer object storage such as S3, R2, or Spaces.\n\n---\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 | Interactive commands try to install Gum locally; install manually from https://github.com/charmbracelet/gum |\n| Framework not detected | Install `jq`, or select manually in wizard |\n\n---\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---\n\n## Documentation\n\nFor detailed guides and reference documentation, see the [docs/](docs/) folder:\n\n- [Getting Started](docs/getting-started/) - Installation, quick start, first deploy\n- [Guides](docs/guides/) - Deployment, configuration, hooks, security, CI/CD\n- [Reference](docs/reference/) - Commands, frameworks, troubleshooting\n- [Examples](docs/examples/) - Express, NestJS, Next.js, React examples\n- [Advanced](docs/advanced/) - Architecture, contributing, distribution\n\n---\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](docs/advanced/architecture.md) for module documentation.\n\n---\n\n## Contributing\n\nShipNode is intentionally simple. Contributions welcome for bug fixes and small improvements.\n\n## License\n\nMIT\n","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"}