Boring Docs

Deployment — Zero-CI Git Push, Private Mesh & Production Ops

The complete guide for humans and autonomous agents to deploy BoringPush on bare-metal VPS or Docker with zero downtime, SQLite persistence, and private Tailscale networking.

Production Deployment & Ops Manual

Deploy BoringPush with zero external CI dependencies, zero hosted runner costs, and zero Docker lock-in. A $5/month Ubuntu VPS + SQLite + Nginx + PM2 serves thousands of daily users with microsecond database responses and zero-downtime rolling reloads.


1. Mental Model: Zero-VPS-CPU Local Build & Rsync Swap

Instead of running slow, resource-heavy builds on your $5 VPS (which spikes CPU and risks Linux OOM crashes), BoringPush defaults to Local Compilation & Atomic Rsync Swap.

Your local workstation (e.g. Mac M-series cores) compiles the production bundle in ~3 seconds into .next_prod. An encrypted transfer over SSH or private Tailscale WireGuard syncs the assets into .next_incoming on the VPS. An atomic filesystem swap (.next_incoming ➔ .next) preserves the previous release in .next_prev for instant 1-second rollbacks (boringpush rollback), and PM2 reloads with zero downtime.

Deploy Engine:

Production Topology

Built on your Mac. Swapped on the VPS in 2s.

Next.js compiles on your workstation cores into a staging slot. Transferred over encrypted Tailscale/SSH with atomic directory swap. VPS CPU stays at ~1%.

  • Zero VPS CPU Spike: heavy compilation runs on local Mac M-cores
  • Atomic Directory Swap: .next_incoming ➔ .next with zero 404s
  • 1-Second Instant Rollback: previous build preserved in .next_prev
  • Smart Edge Purge: Cloudflare invalidates only modified HTML routes
Mode: Rsync Swap
pipeline: scripts/deploy/ship.sh (local build)
Step 1 of 4
VISITORSHTTPS / TLSCloudflare EdgeWAF · Smart Cache PurgeYOUR VPS · PORTS 22 / 80 / 443 CLOSEDcloudflaredinbound tunnelNext.js :3000PM2 · zero-downtimeSQLite0ms WAL file[1] .next_incoming──(atomic swap)──►[2] live .nextBackup: .next_prev preserved for 1-second instant rollback (boringpush rollback)YOUR MAC (LOCAL)NEXT_DIST_DIR=.next_prod0% VPS CPU · M-series coresTailscale WireGuard / SSHrsync -az --delete (~1.9s)

01 · Local Compilation (Workstation)

Uses your machine's M-series multi-cores to compile production bundles into .next_prod. The VPS never runs npm run build, completely eliminating out-of-memory crashes.

0% VPS Load~3.8s
$ NEXT_DIST_DIR=".next_prod" npm run build

↳ Protected: VPS stays cool and never drops visitor connections.

Green: visitor traffic · Amber: zero-CPU code syncboringpush rollback
┌─────────────────────────────────────────────────────────────┐
│ 1. LOCAL WORKSTATION (Mac M-Cores / Linux Dev Machine)      │
│    NEXT_DIST_DIR=".next_prod" npm run build (~3.8s)         │
│    ↳ Heavy compilation on workstation — 0% VPS CPU load    │
└──────────────────────────────┬──────────────────────────────┘
                               │ (SSH or Tailscale WireGuard Mesh)
                               ▼
┌─────────────────────────────────────────────────────────────┐
│ 2. ENCRYPTED TRANSPORT (~1.9s)                              │
│    rsync -az --delete .next_prod/ VPS:.next_incoming/       │
│    ↳ Syncs to isolated staging slot — live site untouched   │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│ 3. VPS RUNTIME (/var/www/boringpush): ATOMIC SWAP (0.02s)   │
│    mv .next .next_prev && mv .next_incoming .next           │
│    ↳ Instant pointer swap (previous build kept for rollback)│
│    ↳ pm2 reload boringpush (zero-downtime process swap)     │
│    ↳ curl health-check -> Cloudflare smart cache purge      │
└─────────────────────────────────────────────────────────────┘

Git Push Fallback: If direct SSH/Tailscale is not available, standard bare Git push (git push production main) remains fully supported as a fallback mode with swapfile protection.


2. Server Provisioning: Two Ways

You need a fresh Ubuntu 22.04 or 24.04 LTS VPS (Hetzner, DigitalOcean, Linode, AWS EC2).

Method A: One-Shot Remote Setup from Your Laptop (Automated)

Run this single command from your local machine repository root:

# Option A (Default: Standard Public IP)
./scripts/deploy/setup.sh \
  --ip 203.0.113.10 \
  --user root \
  --app myapp \
  --domain example.com

# Option B (Tailscale Private Mesh)
./scripts/deploy/setup.sh \
  --ip my-vps \
  --user root \
  --app myapp \
  --tailscale

What setup.sh handles automatically:

  1. Uploads server provisioning scripts to /tmp.
  2. Creates the non-root deploy system user.
  3. Provisions a 2GB swapfile (guarantees Next.js builds never crash from Linux OOM).
  4. Installs Node.js 26.x, PM2, Nginx, UFW, and Git. No compiler toolchain: SQLite is Node's built-in node:sqlite.
  5. Configures PM2 systemd autostart (pm2 startup) so the app survives server reboots.
  6. Sets up Git bare repo at /var/repo/myapp.git with the executable post-receive hook.
  7. Creates /var/www/myapp, configures Nginx reverse proxy with HTTPS via Certbot.
  8. Registers the production remote in your local repository and installs .git/hooks/pre-push.

Method B: Manual Setup Directly on the Server

If you prefer SSHing into the VPS and running the setup by hand:

# 1. SSH into the server as root
ssh root@<vps-ip>

# 2. Run the server setup script
curl -fsSL https://raw.githubusercontent.com/your-org/your-repo/main/scripts/deploy/server-setup.sh -o server-setup.sh
APP_NAME="myapp" APP_USER="deploy" DOMAIN="example.com" bash server-setup.sh

# 3. On your local machine, link the remote and pre-push gate
git remote add production deploy@<vps-ip>:/var/repo/myapp.git
cp scripts/deploy/pre-push .git/hooks/pre-push && chmod +x .git/hooks/pre-push

3. Networking: Public IP vs Tailscale

Option A: Standard Public IP & SSH (Default)

The classic, zero-tooling setup. The server listens on public port 22.

  1. Authorize your local public key:

    ssh-copy-id deploy@<vps-ip>
  2. Add the Git remote:

    git remote add production deploy@<vps-ip>:/var/repo/myapp.git
  3. Deploy:

    git push production main

Option B: Tailscale Private Mesh (Recommended)

Connecting your VPS to Tailscale creates an encrypted private mesh and is the gold standard for solo founders and autonomous agents.

🛡️ Why Tailscale is Strongly Recommended

  • Zero Public Attack Surface: You can close port 22 on the public internet completely (ufw delete allow OpenSSH and ufw allow in on tailscale0). Automated bot scanners, dictionary attacks, and SSH brute-force attempts can never reach your server.
  • Identity-Based SSH (tailscale up --ssh): Eliminates SSH key management. Access is controlled via your identity provider (Google, GitHub, Microsoft). No copying or rotating authorized_keys.
  • Deploy from Anywhere (Mobile / Cellular / Travel): The server gets a permanent 100.x mesh IP and MagicDNS hostname (e.g. myapp-box). You can deploy or SSH from coffee shop Wi-Fi, hotel networks, or your iPhone/iPad via Termius—even if your cloud provider changes public IPs.
  • End-to-End WireGuard Encryption: All Git transport and command sessions run through an encrypted point-to-point tunnel.

Quick Tailscale Setup

# 1. On your server:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --ssh

# 2. Close public SSH on the firewall (100% private):
sudo ufw delete allow OpenSSH
sudo ufw allow in on tailscale0
sudo ufw reload

# 3. On your local machine (or mobile phone):
git remote add production deploy@<tailscale-machine-name>:/var/repo/myapp.git
git push production main

Option C: Cloudflare Tunnel + Tailscale (Zero Public Open Ports)

When combining Cloudflare Tunnel (cloudflared) with Tailscale, your VPS has zero inbound ports open to the public internet (no port 80, 443, or 22):

  • Inbound Public Traffic: Cloudflare Tunnel securely forwards HTTPS requests from dev.boringframework.com directly to localhost:3000 via an outbound encrypted tunnel.
  • Inbound Private Admin & Git: Tailscale WireGuard provides encrypted access for SSH terminal commands and git push production main deployments.
  • Firewall Configuration: You can completely drop public incoming traffic with sudo ufw default deny incoming && sudo ufw allow in on tailscale0.

(See the complete dedicated handbook in docs/DEPLOYMENT_DOCKERLESS_VPS.md).


4. Environment Variables & Secrets (.env)

Secrets never live in Git and are never modified or overwritten by deployments.

Automatic Copy during Setup

If .env.prod is present on your local machine (or if you pass --env .env.prod), setup.sh automatically copies it to /var/www/myapp/.env and locks permissions to chmod 600.

./scripts/deploy/setup.sh --ip 203.0.113.10 --user root --app myapp --domain example.com --env .env.prod

Manual Configuration (Optional)

You can also create or edit /var/www/myapp/.env directly on the server:

# SSH into the server:
ssh deploy@<vps-ip-or-tailscale-name>

# Edit the environment file:
nano /var/www/myapp/.env

Production .env Checklist

NODE_ENV=production
NEXTAUTH_URL=https://example.com
NEXTAUTH_SECRET=generate-a-random-64-character-secret-string
DB_PROVIDER=sqlite
SQLITE_PATH=/var/www/myapp/data/app.db
BILLING_PROVIDER=stripe
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
[email protected]

Data Safety Guarantee: Because .env and *.db / *.sqlite are listed in .gitignore, Git checkout ignores them entirely during deployments. Your production database and live API keys are preserved across every push.


5. Under the Hood: Production Hardening

Our deployment scripts include several critical production safeguards:

1. Swap Space (OOM Killer Protection)

Next.js production builds (next build) compile SWC bundles and generate pages, occasionally spiking RAM usage above 1GB. On a $5 VPS with 1GB RAM, Linux will kill the build process (Killed: 9). server-setup.sh automatically provisions a 2GB swapfile if none exists, ensuring builds always complete smoothly.

2. No Native Modules

BoringPush uses Node's built-in node:sqlite, so npm ci compiles nothing and server-setup.sh installs no compiler toolchain.

3. Server Reboot Survival

server-setup.sh installs and enables PM2 via systemd (pm2 startup systemd -u deploy). If the host VPS reboots or updates its kernel, PM2 automatically restarts your Next.js application upon boot.

4. Git Environment Variable Hygiene

Standard Git hooks set GIT_DIR=".". If unhandled, child processes run within the hook think the current folder is a bare repo. Our post-receive hook explicitly executes unset GIT_DIR and unset GIT_WORK_TREE after checkout, ensuring all build tools and scripts run in a clean environment.


6. Daily Deployment & Verification

Every deployment follows the same workflow:

git push production main
  1. Pre-push verification: .git/hooks/pre-push runs node --check, npm run lint, and tests on your machine.
  2. Server execution: The server receives commits, checks out files to /var/www/myapp, runs npm ci and npm run build, and calls pm2 reload myapp.
  3. Live confirmation: PM2 flips traffic over gracefully once the new build is listening on port 3000.

7. Database Backups & Optimization

Add these cron jobs to the deploy user's crontab on the server (crontab -e):

# Hourly SQLite snapshot with WAL safety (keeps last 24 backups):
0 * * * * cd /var/www/myapp && npm run backup:db

# Daily analytics cleanup:
30 2 * * * cd /var/www/myapp && npm run analytics:cleanup

# Weekly database VACUUM & PRAGMA optimize:
0 3 * * 0 cd /var/www/myapp && npm run db:optimize

8. Agent Operations & Troubleshooting

When an autonomous AI agent is tasked with inspecting, repairing, or deploying BoringPush:

Inspection Checklist

# 1. Check if the app is alive
curl -I http://127.0.0.1:3000

# 2. Check PM2 process table and memory
pm2 status

# 3. View real-time logs
pm2 logs myapp --lines 50

# 4. Check Nginx reverse proxy configuration
sudo nginx -t

# 5. Check firewall rules
sudo ufw status verbose

Common Issues & Remedies

Issue Root Cause Remedy
git push rejected locally pre-push hook failed lint or build Run npm run lint locally and resolve all warnings/errors.
Killed during npm run build Out of Memory (OOM) Run swapon --show. If empty, create 2GB swap via fallocate -l 2G /swapfile && mkswap /swapfile && swapon /swapfile.
502 Bad Gateway on Nginx Node process is down or crashing on boot Check pm2 logs myapp. Verify /var/www/myapp/.env exists and contains valid NEXTAUTH_SECRET.
Permission denied on Git push Deploy user lacks permissions Run sudo chown -R deploy:deploy /var/repo/myapp.git /var/www/myapp.
Port 22 unreachable Firewall blocking or Tailscale down If using Tailscale, ensure tailscale status is active. If public, verify provider Security Group allows port 22.