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
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-CI Git Push to Deploy
Instead of piping code through third-party CI runners (GitHub Actions, Vercel, Railway), your server hosts a bare Git repository. Pushing to it triggers an in-place build and PM2 process swap.
┌─────────────────────────────────────────────────────────────┐
│ 1. LOCAL MACHINE (Laptop / Remote Dev Box) │
│ git push production main │
│ ↳ .git/hooks/pre-push gates push (lint + tests + build) │
└──────────────────────────────┬──────────────────────────────┘
│ (SSH or Tailscale WireGuard)
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. YOUR SERVER: Bare Repo (/var/repo/myapp.git) │
│ ↳ hooks/post-receive fires automatically │
│ ↳ Checks out tracked files to /var/www/myapp │
│ ↳ Leaves .env and database files 100% untouched │
└──────────────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. WORKTREE (/var/www/myapp): scripts/deploy/deploy │
│ ↳ npm ci (installs native modules like better-sqlite3) │
│ ↳ npm run build (protected against OOM by swap) │
│ ↳ pm2 reload myapp (zero-downtime graceful process swap) │
└─────────────────────────────────────────────────────────────┘
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:
- 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 20.x, PM2, Nginx, UFW, Git, and build essentials (
gcc,g++,make,python3forbetter-sqlite3). - 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-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.
- 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.git - 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 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 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.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.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
.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. Native Module Compilation
BoringPush uses better-sqlite3, a high-performance C++ SQLite binding. server-setup.sh installs build-essential (gcc, g++, make) and python3, guaranteeing npm ci compiles native addons seamlessly on Ubuntu.
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: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. |