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.
In this guide
- 1. Mental Model: Zero-VPS-CPU Local Build & Rsync Swap
- 2. Server Provisioning: Two Ways
- 3. Networking: Public IP vs Tailscale
- 4. Environment Variables & Secrets (.env)
- 5. Under the Hood: Production Hardening
- 6. Daily Deployment & Verification
- 7. Database Backups & Optimization
- 8. Agent Operations & Troubleshooting
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.
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
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.
$ NEXT_DIST_DIR=".next_prod" npm run build↳ Protected: VPS stays cool and never drops visitor connections.
┌─────────────────────────────────────────────────────────────┐
│ 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 \
--tailscaleWhat setup.sh handles automatically:
- Uploads server provisioning scripts to
/tmp. - Creates the non-root
deploysystem user. - Provisions a 2GB swapfile (guarantees Next.js builds never crash from Linux OOM).
- Installs Node.js 26.x, PM2, Nginx, UFW, and Git. No compiler toolchain: SQLite is Node's built-in
node:sqlite. - Configures PM2 systemd autostart (
pm2 startup) so the app survives server reboots. - Sets up Git bare repo at
/var/repo/myapp.gitwith the executablepost-receivehook. - Creates
/var/www/myapp, configures Nginx reverse proxy with HTTPS via Certbot. - Registers the
productionremote 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-push3. Networking: Public IP vs Tailscale
Option A: Standard Public IP & SSH (Default)
The classic, zero-tooling setup. The server listens on public port 22.
Authorize your local public key:
ssh-copy-id deploy@<vps-ip>Add the Git remote:
git remote add production deploy@<vps-ip>:/var/repo/myapp.gitDeploy:
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 OpenSSHandufw 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 rotatingauthorized_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 mainOption 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.comdirectly tolocalhost:3000via an outbound encrypted tunnel. - Inbound Private Admin & Git: Tailscale WireGuard provides encrypted access for SSH terminal commands and
git push production maindeployments. - 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.prodManual 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/.envProduction .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
.envand*.db/*.sqliteare 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- Pre-push verification:
.git/hooks/pre-pushrunsnode --check,npm run lint, and tests on your machine. - Server execution: The server receives commits, checks out files to
/var/www/myapp, runsnpm ciandnpm run build, and callspm2 reload myapp. - 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:optimize8. 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 verboseCommon 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. |