Deployments

Self-Hosted Deployment

Build, migrate, and run VitNode on your own server or Docker container with a reverse proxy.

VitNode compiles into a standard Node.js server and static assets. The only required dependency is a PostgreSQL database (Redis is optional for caching and multi-instance scaling).

Quick start

Run these 4 commands on your deployment server:

Deploy VitNode
bun install --frozen-lockfile
bun run build
bun run db:migrate
bun start

Always run db:migrate before start

Production processes do not automatically run migrations at boot. Running db:migrate ensures the database schema matches your compiled application before traffic is served.


Required Environment Variables

Configure these in your production .env file:

.env
# Database (PostgreSQL 15+)
POSTGRES_URL=postgresql://user:password@localhost:5432/vitnode_db

# The origin visitors reach. Email links, SSO callbacks and password-reset
# links are built from it.
VITNODE_WEB_URL=https://example.com

# Required if anything schedules cron. A long random string.
CRON_SECRET=change_me_to_a_long_random_secret_string

# Optional: Shared Cache & Realtime
REDIS_URL=redis://localhost:6379
REDIS_PASSWORD=change_me_redis_password

Where the API lives

A Single App answers /api/* itself, so leave VITNODE_API_URL unset. The server takes the API origin off the request being rendered and the browser falls back to the origin the page was served from, so the same build is correct on every hostname you point at it.

A split deployment runs the web app and apps/api as two processes, and the web app then has to be told where the API is - its own origin has no /api/* to answer, so an unset value turns every server-side call into a 404, starting with the session read on the first render. Set it in the web app's environment, and set it before the build: it is compiled into the browser bundle too.

apps/web/.env
VITNODE_API_URL=http://localhost:8000

A value set here wins over the request origin, which is why a Single App must not carry one - http://localhost:3000 left over from development would point production at the visitor's own machine.


Reverse Proxy (Nginx Configuration)

Proxy traffic to VitNode while forwarding client headers and supporting WebSocket upgrades:

/etc/nginx/sites-available/vitnode.conf
server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;

        # WebSocket support
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Forwarded identity
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Running with PM2 (Process Manager)

Keep VitNode running continuously with automatic restarts on crash:

Run VitNode with PM2
# Install PM2
bun add -g pm2

# Start both web and API processes
pm2 start "bun start" --name "vitnode-web"
pm2 save
pm2 startup

Learn More