Skip to content

vercel-labs/flood

Repository files navigation

Flood

A Self-Hosting Lab for Next.js

What does it really take to self-host a modern Next.js app on a VPS? Flood is an open, reproducible lab for finding out: a monorepo containing a Next.js 16 blog platform, infrastructure-as-code to deploy it to a VPS, and k6 load tests to understand how it behaves under real traffic.

This is not a benchmark and not a framework comparison. Some apps run perfectly well on a $5 VPS with basic tuning. The goal is to give you the tools to measure and understand your own setup — whether you stay self-hosted or decide a managed platform makes more sense for your use case.

Prerequisites

The app itself only needs Node.js and pnpm. The CLI tools for provisioning, VMs, and load testing require:

  • Node.js 22+ and pnpm 10+corepack enable && corepack install
  • Ansiblebrew install ansible — provisions and deploys to servers
  • One of:
    • Multipassbrew install multipass — local Ubuntu VMs (optional, for local testing)
    • doctlbrew install doctl — DigitalOcean CLI (optional, only for flood vps)
  • k6brew install k6 — load testing (optional, only for flood load)

Quick Start

pnpm install
pnpm build
pnpm dev     # starts the Next.js app locally (SQLite, no external deps)

# Seed 1000 articles
curl -X POST http://localhost:3000/api/seed \
  -H "Content-Type: application/json" -d '{"count": 1000}'

CLI

Link the CLI for global access:

cd apps/cli && pnpm link --global
flood --help

Turbo Tasks

All project-wide commands run through turbo:

pnpm build       # build all packages
pnpm dev         # start dev servers (in-memory cache)
pnpm dev:redis   # start dev servers with Redis cache handler
pnpm test        # run all tests
pnpm typecheck   # type-check all packages
pnpm clean       # clean build outputs

Cache Handlers

By default, "use cache" directives use Next.js's built-in in-memory LRU cache. Set FLOOD_CACHE_HANDLER=redis to use a Redis-backed cache handler instead — useful for multi-instance deployments where cache needs to be shared.

# Start Redis + dev server in one command
pnpm dev:redis

# Or manually
docker compose -f docker-compose.dev.yaml up -d   # starts Redis on :6379
FLOOD_CACHE_HANDLER=redis REDIS_URL=redis://localhost:6379 pnpm dev

The Redis handler lives at apps/web/src/lib/cache-handlers/redis.js and implements the Next.js 16 cacheHandlers interface (get, set, refreshTags, getExpiration, updateTags).

What's Inside

Package Description
apps/web Next.js 16 multi-author blog (the demo app being load tested)
apps/cli CLI for provisioning, deploying, and running load tests
packages/config Shared Zod schemas and env var definitions
packages/tsconfig Shared TypeScript configs
infra/ansible Ansible roles and playbooks for server provisioning
infra/multipass Cloud-init config for local Ubuntu VMs
k6/ k6 load test scripts (general + 7 dedicated scenarios)

The Demo App

A multi-author blog with 10K+ generated articles. Exercises every Next.js self-hosting feature:

  • ISR with use cache — cached article, author, and tag pages
  • Custom cache handler — Redis-backed shared cache for multi-instance deployments
  • Image optimizationnext/image with external sources (picsum.photos)
  • OG image generationnext/og for dynamic social images
  • Streaming — React Suspense on the feed page
  • Server Actions — newsletter signup form
  • Search — full-text search, fully dynamic
  • API routes — health check, metrics, seeding

Load Testing

flood load <target>                              # realistic traffic (50 VUs, 5 minutes)
flood load <target> --stress                     # ramp to 500 VUs
flood load <target> --scenario cache-exhaustion  # target a specific failure mode

There are 7 dedicated scenario scripts in k6/scenarios/, each targeting a specific self-hosting behavior. See Scenarios for full details.

Deployment Options

Local VM (Multipass)

flood vm create my-vm            # create Ubuntu VM with cloud-init
IP=$(flood vm ip my-vm)          # get the VM's IP address
flood provision $IP              # install Node, PostgreSQL, Caddy, Beszel
flood build                      # build the app locally
flood deploy $IP                 # rsync build to VM, start with systemd
flood seed $IP                   # seed the database

For local VMs, HTTPS is automatic — the CLI detects the .flood.local domain from /etc/hosts and Caddy provisions a self-signed cert. Access the app at https://my-vm.flood.local.

For public domains, pass --domain explicitly:

flood provision $IP -d flood.example.com   # Let's Encrypt cert (public domains)

DigitalOcean VPS

flood vps create my-vps --tier tier1   # create a DO droplet
IP=$(flood vps ip my-vps)             # get the IP
flood provision $IP                    # provision the server
flood build                            # build the app locally
flood deploy $IP                       # deploy to the droplet
flood seed $IP                         # seed the database
flood load $IP                         # run load tests
flood vps destroy my-vps               # tear down when done (stops billing)

Requires doctl authenticated with your DigitalOcean account. Tiers: tier1 ($6/mo, 1 vCPU/1GB), tier2 ($12/mo, 1 vCPU/2GB), tier3 ($24/mo, 2 vCPU/4GB). Use flood vps destroy-all --force to clean up all flood droplets.

Remote VPS (manual)

Same workflow as above, just use your server's IP directly. The default SSH user is root; override with -u if needed.

flood provision 203.0.113.10                          # plain HTTP on :80
flood provision 203.0.113.10 -d flood.example.com     # automatic HTTPS via Let's Encrypt
flood provision -u ubuntu 203.0.113.10                # different SSH user
flood build
flood deploy 203.0.113.10

SSH Authentication

All commands use Ansible, which connects over SSH. Ansible uses your default SSH key (~/.ssh/id_ed25519, ~/.ssh/id_rsa, etc.) or whichever key is loaded in your SSH agent — no extra config needed.

For a remote VPS, make sure your public key is on the server first (most providers let you add it at creation time). For Multipass VMs, SSH access is handled automatically.

System Monitoring

Beszel provides lightweight system metrics (CPU, memory, disk, network) via a web dashboard. It's pre-configured during provisioning — no manual setup needed.

Open http://<target>:8090 and log in with admin@flood.local (password from FLOOD_BESZEL_PASSWORD or default flood_beszel_dev). The local system is already registered and collecting metrics.

Running a Load Test

1. Set up monitoring

Open Beszel at http://<target>:8090 before starting the test so you can watch metrics in real time.

2. Run the test

flood load <target>                                    # realistic traffic (50 VUs, 5 min)
flood load <target> --stress                           # ramp to 500 VUs
flood load <target> --vus 200 --duration 10m           # custom
flood load <target> --scenario cache-exhaustion        # target a specific failure mode
flood load <target> --scenario isr-revalidation   # target a specific behavior

k6 prints live stats as the test runs and a summary at the end. Dedicated scenarios have their own defaults for VUs and duration but accept --vus and --duration overrides.

3. What to watch for

Metric Where Warning sign
http_req_duration (p95) k6 output > 500ms means the server is struggling
http_req_failed k6 output Any non-zero failure rate
CPU usage Beszel / htop Sustained > 90% = bottleneck
Memory usage Beszel Node process > 80% of available RAM
PostgreSQL connections journalctl -u flood-web "too many connections" errors
Reverse proxy queue Beszel network graph Flat throughput + rising latency = saturation

4. Common failure modes

  • CPU-bound: p95 latency climbs linearly with VUs. OG image generation and uncached SSR are the most expensive operations.
  • Memory pressure: Node process gets OOM-killed by the OS. Check dmesg | grep -i oom after a crash.
  • Connection pool exhaustion: Database errors in app logs. Increase FLOOD_DB_POOL_SIZE in the .env.
  • Response buffering: If streaming responses (Suspense) feel slow, verify flush_interval -1 is set in the Caddyfile.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors