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-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:

  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 20.x, PM2, Nginx, UFW, Git, and build essentials (gcc, g++, make, python3 for better-sqlite3).
  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. 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
  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.