Skip to content

Repository files navigation

🛫 PushPilot

A lightweight, self-hosted deploy dashboard. Git-pull, build, and reload your apps from a browser — no SSH needed. Point it at your server's apps, click Deploy, watch the logs stream live.

Built for the common small-VPS setup: a few Node/PM2 services and some static builds, all deployed from one box. One small Node process, one config file, zero external services.

⚠️ PushPilot runs shell commands on your server. It is a powerful tool and a potential backdoor if misconfigured. Read the Security section before exposing it to the internet.


Features

  • One-click deploys — each app is a named pipeline of steps (git pull → install → build → reload).
  • Auto-discovery — scan your app dirs, see detected apps (git/PM2/build), click Add to make them deployable. No hand-writing config.
  • Server monitoring — live CPU, RAM, disk usage per mount, load average, uptime, OS/Node info.
  • Full PM2 control — start / stop / restart / reload / delete / flush / scale any process, with live per-process log tailing.
  • System services — view + start/stop/restart whitelisted systemd services (nginx, mongod, …).
  • Deploy history + git status — every deploy is recorded (time, result, duration, commit); each app shows its current commit and how many commits it's behind origin.
  • Add / remove projects — register a new app from the UI (name, dir, git, steps, PM2) — no hand-editing config.
  • MongoDB management — connection status, list databases & collections with counts/sizes, one-click mongodump backups.
  • Nginx management — list sites, view/edit configs, enable/disable, nginx -t, reload.
  • SSL / domains — list Let's Encrypt certs + expiry, renew, and issue new certs via certbot --nginx.
  • Sidebar UI — a proper multi-page dashboard (Dashboard, Apps, Processes, MongoDB, Nginx, SSL, Services, Settings), responsive.
  • Live log streaming — watch every command's output in real time over WebSocket.
  • PM2 integration — see process status, memory, uptime, restarts; reload with a button.
  • Config-driven — describe your apps in apps.json. Nothing hardcoded.
  • Injection-safe — steps are ["cmd", "arg", ...] argv arrays run with no shell.
  • Token auth + optional IP allowlist — simple, effective, no user database.
  • No build step for the UI — plain HTML/JS, served as static files.
  • Tiny footprint — two dependencies (express, ws).

How it works

Browser ──(token)──▶ PushPilot (Node/Express on your VPS)
                        │
                        ├─ POST /api/apps/:id/deploy ─▶ runs the app's steps with spawn() (no shell)
                        ├─ GET  /api/pm2             ─▶ pm2 jlist (status table)
                        ├─ POST /api/apps/:id/restart─▶ pm2 reload <name>
                        └─ WebSocket                 ─▶ streams stdout/stderr live

Each app in apps.json is a whitelist entry. The browser can only ask PushPilot to run apps that already exist in that file, with the exact steps you defined — it can never send an arbitrary command.


Auto-discovery (optional)

Don't want to hand-write apps.json? Point PushPilot at where your apps live and let it find them.

  1. Set SCAN_DIRS in .env (comma-separated roots, scanned one level deep):
    SCAN_DIRS=/var/www,/home/deploy
    
  2. Click 🔍 Discover apps in the dashboard. PushPilot lists every git repo / npm project it found, auto-detecting the branch, a matching PM2 process, and whether there's a build script — and proposes a sensible deploy pipeline for each.
  3. Tweak the id if you like and click Add. That writes the app into apps.json and it becomes deployable.

Discovery never runs anything. It only reads the filesystem and pm2 jlist to make suggestions. A candidate becomes runnable only when you explicitly adopt it — you are the approval gate. Adopted apps are re-validated (argv-only steps) and must live inside a configured SCAN_DIRS root, so a tampered request can't smuggle in an arbitrary path or a shell command. apps.json remains the single source of truth for what's allowed.


Managing your server

Beyond deploys, the dashboard gives you live control of the box:

  • Server stats (always on): CPU, memory, disk usage per mount, load average, uptime.
  • PM2 processes: every process is listed with live CPU/mem/status. Buttons to restart, reload, stop/start, flush logs, delete, and tail live logs. By default you can only control processes tied to a configured app; set PM2_MANAGE_ALL=true to manage every pm2 process on the machine.
  • System services: list SERVICES=nginx,mongod in .env and the dashboard shows their status with start/stop/restart/reload buttons (via systemctl).
  • Deploy history & git status: each app card shows its current commit and how far behind origin it is; the History button lists past deploys (time, result, duration, commit).

systemctl needs privileges. For the services panel to actually control units, the user running PushPilot needs permission — e.g. a polkit rule or a scoped sudoers entry like:

pushpilot ALL=(root) NOPASSWD: /bin/systemctl restart nginx, /bin/systemctl reload nginx

Grant only the units + actions you need. Status reads (is-active) usually need no sudo.


Quick start

git clone https://github.com/YOU/pushpilot.git
cd pushpilot
npm install

cp .env.example .env
cp apps.example.json apps.json

# 1) Generate a strong token and put it in .env
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
#   → paste into PUSHPILOT_TOKEN=...

# 2) Edit apps.json to describe YOUR apps (see below)

# 3) Run it
npm start
# 🛫 PushPilot running at http://127.0.0.1:4700

Open the URL, paste your token, and you're in. To keep it running:

npm install -g pm2
pm2 start ecosystem.config.js
pm2 save && pm2 startup

Configuration

.env

Var Default Purpose
PUSHPILOT_TOKEN — (required) The access token. Make it long and random.
PORT 4700 Port to listen on.
HOST 127.0.0.1 Bind address. Keep local if nginx proxies to it.
ALLOWED_IPS (empty) Comma-separated IP allowlist. Empty = any IP (rely on token).
APPS_CONFIG ./apps.json Path to your apps file (auto-created empty if missing).
SCAN_DIRS (empty) Comma-separated roots for auto-discovery. Empty = disabled.
SCAN_DEPTH 3 How deep discovery descends (catches nested layouts).
PM2_MANAGE_ALL false Allow controlling ANY pm2 process, not just app-linked ones.
SERVICES (empty) Comma-separated systemd units to manage (e.g. nginx,mongod).
MONGO_URI (empty) Enables the MongoDB page. Empty = page hidden.
BACKUP_DIR ./backups Where mongodump backups are written.
NGINX_SITES_AVAILABLE (empty) Enables the Nginx page (e.g. /etc/nginx/sites-available).
NGINX_SITES_ENABLED (empty) For enable/disable via symlink (e.g. /etc/nginx/sites-enabled).
TRUST_PROXY 0 Set to 1 when behind nginx so the real client IP is used.

The SSL page uses certbot directly — no env needed; it just has to be installed. Panels for MongoDB / Nginx / SSL only appear when their prerequisites are present. certbot, systemctl, and writing to /etc/nginx generally require the PushPilot user to have the right privileges (root, or scoped sudoers/polkit rules).

apps.json

Copy apps.example.json and describe each app. Steps are argv arrays — never shell strings.

{
  "apps": [
    {
      "id": "api",
      "name": "Backend API",
      "description": "Node service under PM2",
      "dir": "/var/www/myproject/backend",
      "branch": "main",
      "pm2": "myproject-api",
      "steps": [
        ["git", "fetch", "origin"],
        ["git", "reset", "--hard", "origin/main"],
        ["npm", "ci", "--omit=dev"],
        ["pm2", "reload", "myproject-api", "--update-env"]
      ]
    },
    {
      "id": "admin",
      "name": "Admin Panel",
      "description": "Vite static build served by nginx",
      "dir": "/var/www/myproject/admin",
      "branch": "main",
      "pm2": null,
      "steps": [
        ["git", "fetch", "origin"],
        ["git", "reset", "--hard", "origin/main"],
        ["npm", "ci"],
        ["npm", "run", "build"],
        ["rsync", "-a", "--delete", "dist/", "/var/www/html/admin/"]
      ]
    }
  ]
}

Fields: id (unique, alphanumeric), name, description, dir (working directory for all steps), branch (display only), pm2 (process name, or null if not a PM2 app), steps (the pipeline). git reset --hard origin/<branch> is recommended over git pull so deploys are deterministic and never hit merge conflicts — but use whatever fits you.


Behind nginx + HTTPS (recommended)

Bind PushPilot to localhost and let nginx terminate TLS:

location /pushpilot/ {
    proxy_pass http://127.0.0.1:4700/;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;      # WebSocket
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $remote_addr;
}

Set TRUST_PROXY=1 in .env so the IP allowlist reads the real client address.


Security

Treat PushPilot like an admin panel with a shell attached — because that's what it is.

  • Strong token. 32+ random bytes. It's the only gate. Rotate it if it leaks.
  • Always HTTPS. Never send the token over plain HTTP. Put it behind nginx/Caddy TLS.
  • Bind to localhost (HOST=127.0.0.1) and proxy through nginx, rather than exposing the port.
  • Use ALLOWED_IPS if you deploy from fixed IPs, or put it behind a VPN / Cloudflare Access / Tailscale.
  • Least privilege. Run PushPilot as a user that can only touch the app dirs it needs — not root.
  • Whitelist only. Only apps in apps.json can run, only with their defined steps. Keep that file tight.
  • No shell. Steps run via spawn(cmd, args, { shell: false }), so tokens are never interpreted by a shell.

PushPilot deliberately has no feature to run arbitrary commands from the browser. If you need a new deploy step, add it to apps.json on the server.


API

All routes require Authorization: Bearer <token> (WebSocket uses ?token=).

Method Path Description
GET /api/me Verify a token.
GET /api/apps List apps + deploy state.
GET /api/pm2 PM2 process table.
POST /api/pm2/:name/:action start/stop/restart/reload/delete/flush/scale a process.
GET /api/system Host CPU/RAM/disk/load/uptime.
GET /api/services Whitelisted service status.
POST /api/services/:name/:action start/stop/restart/reload a service.
GET /api/discover Read-only scan of SCAN_DIRS.
POST /api/discover/adopt Persist a discovered app.
GET /api/apps/:id/logs Recent buffered logs for an app.
GET /api/apps/:id/history Deploy history for an app.
GET /api/apps/:id/git Git status (commit, behind-by-N).
POST /api/apps/:id/deploy Start a deploy.
POST /api/apps/:id/restart pm2 reload the app's process.
WS /?token=… Live deploy log + state stream.
WS /?token=…&logs=<proc> Live tail of a pm2 process.

Contributing

Issues and PRs welcome. Keep the core small and dependency-light. Please don't add a feature that lets the browser run arbitrary commands — the whitelist model is the point.

License

MIT © PushPilot contributors

Note: "PushPilot" is a common name; if you publish to npm you may need a scoped name like @yourname/pushpilot. The GitHub repo name is up to you.

About

Self-hosted deploy dashboard — git pull, build & PM2 reload your apps from the browser, no SSH.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages