Boring Docs
Why Ditch Vercel & The Zero-Open-Port VPS Manual (Cloudflare Tunnel + Tailscale)
The complete guide to quitting the $100/mo Vercel tax, setting up a zero-attack-surface VPS with Tailscale and Cloudflare Tunnel, and battle-tested troubleshooting for real-world issues.
In this guide
- 1. Why Ditch Vercel, Serverless & Cloud DBs?
- 2. The Zero-Attack-Surface Architecture
- 3. How to Set Up Cloudflare Tunnel (In 3 Minutes)
- 4. How to Set Up Tailscale (In 2 Minutes)
- 5. Battle-Tested Field Guide: Common Issues & Exact Fixes
- 6. How Mac-to-Linux Deployment Works (Architecture Deep Dive)
- 7. Daily Deployment Cheat Sheet
Why Ditch Vercel & The Zero-Open-Port VPS Manual
$5/Month Total Cost • Zero Open Public Ports • 0.1ms SQLite Queries • Zero Docker Lock-In
1. Why Ditch Vercel, Serverless & Cloud DBs?
Most modern boilerplates push founders into the "Modern Cloud Stack": Vercel + Neon Postgres + Upstash Redis + Datadog/Sentry + Clerk Auth.
Before you make your first dollar, you are locked into a fragile, multi-vendor subscription trap:
The Real Cost Comparison
| Problem | The Vercel + Cloud DB Stack | The BoringPush VPS Stack |
|---|---|---|
| Monthly Hosting Bill | $20/seat + bandwidth overages ($50–$200+/mo) | $4–$5/mo flat (Hetzner, DigitalOcean, Linode) |
| Database Cost | $29/mo (Neon / Supabase compute add-on) | $0 (Embedded SQLite on NVMe SSD) |
| Query Latency | 35ms–100ms per query across cloud network hops | 0.1ms (In-memory / local disk read) |
| Connection Limits | Serverless function connection pooling exhaustion (500 errors) | Zero connection limits (Direct file descriptor) |
| Cold Starts | 800ms–2.5s cold boots on lambdas | 0ms (PM2 persistent Node.js process) |
| Execution Timeouts | Hard 10s–15s function timeouts kill long tasks | Unlimited (Long-running workers & exports run fine) |
| Attack Surface | Complex IAM permissions, public endpoints | 0 public open ports (Invisible to port scanners) |
The Latency Truth
When your Next.js serverless route executes 3 database queries to render a page, each query incurs a TCP roundtrip to AWS us-east-1 or Supabase:
$$3 \text{ queries} \times 35\text{ms} = 105\text{ms of pure network waiting}.$$
On a $5/mo VPS with SQLite:
$$3 \text{ queries} \times 0.1\text{ms} = 0.3\text{ms total}.$$
Your pages render and return before a serverless function even finishes its SSL handshake.
2. The Zero-Attack-Surface Architecture
How do you run on a bare VPS safely without becoming a full-time DevOps engineer?
We combine two battle-tested zero-trust tools:
VISITORS (Public Web)
│
▼ HTTPS request (boringpush.com)
┌───────────────────────────┐
│ Cloudflare Edge Network │
└─────────────┬─────────────┘
│ Outbound Encrypted Tunnel (Zero inbound ports)
▼
┌────────────────────────────────────────────────────────────────────────────────┐
│ YOUR VPS (Port 22, 80, 443 are 100% CLOSED on the public internet) │
│ │
│ ┌──────────────────────────┐ ┌────────────────────────────────────┐ │
│ │ cloudflared daemon │────────▶│ Next.js App (127.0.0.1:3000) │ │
│ │ (Inbound Web Traffic) │ HTTP │ Supervised by PM2 (zero-downtime) │ │
│ └──────────────────────────┘ └─────────────────┬──────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ SQLite DB (/var/www/app/db/app.db) │ │
│ │ 0ms latency, persistent on disk │ │
│ └────────────────────────────────────┘ │
│ ▲ │
│ │ build & reload │
│ ┌──────────────────────────┐ ┌─────────────────┴──────────────────┐ │
│ │ Tailscale (WireGuard) │────────▶│ Bare Git Repo (/var/repo/app.git) │ │
│ │ (Private Admin & Deploy) │ SSH │ post-receive hook │ │
│ └──────────────────────────┘ └────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────────┘
▲
│ git push production main
│ Encrypted WireGuard Mesh (100.x.y.z)
┌─────────────┴─────────────┐
│ YOUR MAC / LAPTOP │
│ Runs local CI gate │
└───────────────────────────┘
- Cloudflare Tunnel (
cloudflared):- Creates an outbound-only connection from your VPS to Cloudflare's global edge.
- Visitors reach
https://boringpush.comwith free automated SSL, DDoS mitigation, and edge caching. - No ports 80 or 443 need to be opened on your VPS firewall.
- Tailscale (Private WireGuard Mesh):
- Assigns your VPS a private, permanent
100.x.y.zIP and MagicDNS name. - Used exclusively by you for terminal SSH and
git push production main. - Port 22 is completely closed to the public internet. Port scanners see a brick wall.
- Assigns your VPS a private, permanent
3. How to Set Up Cloudflare Tunnel (In 3 Minutes)
- Go to your Cloudflare Zero Trust Dashboard.
- Navigate to Networks → Tunnels and click Create a Tunnel.
- Select Cloudflared, name it (e.g.
vps-tunnel), and save. - Under Install and run a connector, select Debian 64-bit and run the provided command on your VPS terminal:
sudo cloudflared service install <YOUR_TUNNEL_TOKEN> - Click Next to go to Public Hostnames:
- Subdomain: Leave blank (for apex domain
boringpush.com) or enter subdomain (e.g.app). - Domain: Select your domain from the dropdown (
boringpush.com). - Type: Select
HTTP. - URL: Type
localhost:3000(or127.0.0.1:3000).
- Subdomain: Leave blank (for apex domain
- Click Save hostname.
4. How to Set Up Tailscale (In 2 Minutes)
- On your VPS, install and enable Tailscale with native SSH:
curl -fsSL https://tailscale.com/install.sh | sh sudo tailscale up --ssh - On your Mac, ensure Tailscale is installed and logged into the same account.
- Verify your connection:
You will see your VPS name and itstailscale status100.x.y.zIP. - Close public SSH on the server firewall completely:
sudo ufw delete allow OpenSSH sudo ufw allow in on tailscale0 sudo ufw reload
5. Battle-Tested Field Guide: Common Issues & Exact Fixes
Here are the real-world gotchas encountered when provisioning a fresh VPS and how to solve them instantly:
Issue 1: Terminal Prompts: "Country of origin for the keyboard"
- What Happens: During
server-setup.sh, the process stops at an interactive Debian/Ubuntu dialogue asking for keyboard layout numbers (35. English (US)). - Why: Ubuntu updates system packages (
keyboard-configuration) interactively by default. - The Fix:
- If stuck in the prompt: Type
35(English US) and press Enter, then1for standard layout. - To prevent it permanently: Always export non-interactive flags before updating:
export DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade -y
- If stuck in the prompt: Type
Issue 2: ./scripts/deploy/setup.sh Hangs at Connecting to ... as coder...
- What Happens: The script prints
Connecting to <IP>...and halts with no output. - Why:
- Tailscale SSH prompts for browser approval on the first connection (
https://login.tailscale.com/a/...), whichscphides. - Or
scpis waiting silently for an SSH password/key passphrase.
- Tailscale SSH prompts for browser approval on the first connection (
- The Fix:
- Cancel the stuck command (
Ctrl + C). - Run a direct interactive SSH connection once:
ssh coder@<TAILSCALE_IP> - If Tailscale gives you an approval URL, click it in your browser. If it asks for a password, enter it.
- Ensure your public SSH key is copied to the server:
ssh-copy-id coder@<TAILSCALE_IP>
- Cancel the stuck command (
Issue 3: Next.js Shows "Internal Server Error" (Static Logo Loads Fine)
- What Happens: Visiting your domain loads the page logo or styles, but renders a generic Next.js "Internal Server Error".
- Checking the Logs:
Shows:sudo -u deploy pm2 logs boringpush --lines 20NextNodeServer.getMiddlewareManifest ... MODULE_NOT_FOUND. - Why: PM2 was started, but the production build has not run yet. The
.next/directory or itsmiddleware-manifest.jsonfile is missing. - The Fix: Run a production build on the server:
# From your Mac: git push production main # Or directly on the server: cd /var/www/boringpush sudo -u deploy npm run build sudo -u deploy pm2 reload boringpush
Issue 4: Out-Of-Memory (OOM) Build Failure (Killed / SIGKILL)
- What Happens:
npm run buildabruptly aborts withKilledon a 1GB or 2GB VPS. - Why: Next.js webpack/SWC compilation can spike memory usage to 1.5GB+ for a few seconds.
- The Fix: Allocate a 2GB Linux swapfile on the VPS (included in BoringPush setup):
sudo fallocate -l 2G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
Issue 5: Google OAuth Error 400: redirect_uri_mismatch
- What Happens: Clicking "Continue with Google" opens Google's sign-in page with
Error 400: redirect_uri_mismatch. - Why:
- Next.js in production (
next start) loads.env.productionwith higher priority than.env. If.env.productionhas a staleNEXTAUTH_URL, it overrides.env. - PM2 retains its initial environment variables across reloads unless invoked with
--update-env. - Google Cloud Console does not have the exact production redirect URI registered.
- Next.js in production (
- The Fix:
- In Google Cloud Console (APIs & Services → Credentials → OAuth 2.0 Client IDs), under Authorised redirect URIs, add:
(Make sure it useshttps://yourdomain.com/api/auth/callback/googlehttps://, has no trailing slash, and includes/api/auth/callback/google). - In your local
.env.production, ensure:NEXTAUTH_URL=https://yourdomain.com - Run the one-command sync from your Mac:
This pushes yournpm run env:sync.env.productionto both.envand.env.productionon the server and reloads PM2 with--update-env.
- In Google Cloud Console (APIs & Services → Credentials → OAuth 2.0 Client IDs), under Authorised redirect URIs, add:
Issue 6: Segmentation fault (core dumped) on SQLite / NextAuth Callback
- What Happens: The server logs show
Segmentation fault (core dumped)immediately when a user attempts to log in or query SQLite. - Why: On modern Linux distros (e.g. Ubuntu 24.04 LTS), Node 20 packages can have an ABI symbol registration mismatch with native C++ modules (
better-sqlite3). - The Fix: Upgrade to Node.js 22 LTS on the VPS:
# 1. Upgrade to Node 22 LTS on the server: curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - sudo apt-get install -y nodejs # 2. Rebuild native modules from source: cd /var/www/boringpush sudo -u deploy npm rebuild better-sqlite3 --build-from-source # 3. Reload PM2: sudo -u deploy pm2 restart boringpush --update-env
6. How Mac-to-Linux Deployment Works (Architecture Deep Dive)
Founders often ask: "If I am on a Mac (Apple Silicon ARM64), how does pushing to an Ubuntu x86_64 server work without breaking? Do I build on the server every time?"
Here is the exact lifecycle:
The Golden Rule: Never Ship Machine Binaries Over Git
- Your Code is 100% Portable: All JavaScript, React JSX, CSS, Tailwind tokens, and Markdown are plain text. They run identically on macOS, Linux, and Windows.
- Native C++ Modules Are Architecture-Specific: Packages with compiled C++ code (like
better-sqlite3,sharp,bcrypt) compile binary.nodemachine code. A binary compiled on Apple Silicon (darwin-arm64) will instantly crash on Linux (linux-x64). - Why We Build on the Linux Server: By pushing pure git source code, the Linux server compiles its own native
.nextbundles and links directly against its host glibc. You never have to deal with Docker multi-arch builds or cross-compilation errors.
Do We Rebuild better-sqlite3 Every Time?
No. Only the very first time!
- When
better-sqlite3is compiled for Node 22 during initial setup, the native binary lives inside/var/www/boringpush/node_modules/. - When you run
git push production main:- Git updates only your changed source files (
app/,components/,config/). node_modules/is never touched or wiped.- The server only runs
next build(compiling your JavaScript and styles in ~15–20 seconds). - PM2 performs a zero-downtime graceful reload (
pm2 reload --update-env).
- Git updates only your changed source files (
The Double-Gate Safety Net
- Gate 1 (Local Pre-Push on Mac): The local Git hook runs
next lintandnext buildon your Mac. If you have a syntax error, broken import, or type failure, the push is blocked on your machine. Bad code never touches production. - Gate 2 (Remote Post-Receive on Linux): The server checks out the clean commit, builds the production bundle natively for Linux x86_64, generates live production sitemaps, and seamlessly switches incoming traffic.
7. Daily Deployment Cheat Sheet
| Task | Command | Where to Run |
|---|---|---|
| Deploy code updates | git push production main |
Local Mac Terminal |
| Sync environment variables | npm run env:sync |
Local Mac Terminal |
| Check server status | sudo -u deploy pm2 status |
VPS Terminal |
| Inspect live logs | sudo -u deploy pm2 logs boringpush |
VPS Terminal |
| Instant SQLite database snapshot | npm run backup:mjs |
Local or VPS |
| One-second rollback to previous commit | git checkout HEAD~1 && pm2 reload boringpush --update-env |
VPS Terminal |