Skip to main content
kRouter
All posts
How kRouter works

Self-host an AI router on a $6/month VPS

Deploy kRouter on a small VPS so your team shares one endpoint: systemd, nginx with streaming, Tailscale-only access, Docker, and the caveats.

Kodelyth · The team behind kRouter
· Updated
5 min read

Running kRouter locally is fine for one developer. If a team shares accounts and combos, one small VPS makes more sense: everyone's tools hit the same endpoint, the same combos and the same dashboard.

Here is the full setup. Budget about 20 minutes.

The VPS

Any small Linux VPS that runs Node 20.9 or newer works. For scale, DigitalOcean's Basic droplets start at $4 a month for 512 MiB of RAM and $6 for 1 GiB (its listed prices in October 2026); other hosts have entry plans of their own, so compare current prices. kRouter does not publish a minimum, but a Next.js server plus a global npm install is tight in 512 MiB, so 1 GiB is the safer floor.

Step 1: Provision

# SSH in
ssh root@your-vps-ip
 
# Install Node 22
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt-get install -y nodejs
 
# Create a non-root user
adduser krouter
usermod -aG sudo krouter
su - krouter
 
# Install kRouter
sudo npm install -g @sifxprime/krouter
which krouter   # note the path for the service file

Step 2: Run as a systemd service

Create /etc/systemd/system/krouter.service, using the path which krouter printed:

[Unit]
Description=kRouter AI router
After=network.target
 
[Service]
Type=simple
User=krouter
WorkingDirectory=/home/krouter
ExecStart=/usr/bin/krouter --skip-update --host 127.0.0.1
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
EnvironmentFile=/etc/krouter.env
 
[Install]
WantedBy=multi-user.target

--host 127.0.0.1 matters. kRouter binds to 0.0.0.0 by default, which would put port 20128 on the public internet next to nginx. The krouter command ignores HOSTNAME and PORT in the environment, so the flags are the way to set them.

Behind nginx every login counts as remote, and the default 123456 is only accepted from the server itself, so give kRouter a starting password. This saves a random one to the file the unit reads and prints it:

echo "INITIAL_PASSWORD=$(openssl rand -base64 18)" | sudo tee /etc/krouter.env && sudo chmod 600 /etc/krouter.env

Then:

sudo systemctl enable krouter
sudo systemctl start krouter
sudo systemctl status krouter

kRouter is now running on 127.0.0.1:20128 and comes back after a reboot.

Step 3: nginx in front

Install nginx and front kRouter with TLS:

sudo apt-get install -y nginx certbot python3-certbot-nginx

/etc/nginx/sites-available/krouter:

server {
    listen 443 ssl http2;
    server_name router.your-domain.com;
 
    ssl_certificate     /etc/letsencrypt/live/router.your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/router.your-domain.com/privkey.pem;
 
    # SSE / streaming-friendly defaults
    proxy_read_timeout 600s;
    proxy_buffering off;
    proxy_cache off;
    proxy_http_version 1.1;
    chunked_transfer_encoding on;
 
    location / {
        proxy_pass http://127.0.0.1:20128;
        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;
    }
}

Enable, certbot, restart:

sudo ln -s /etc/nginx/sites-available/krouter /etc/nginx/sites-enabled/
sudo certbot --nginx -d router.your-domain.com
sudo systemctl reload nginx

You now have https://router.your-domain.com serving kRouter. Streaming works because of the proxy_buffering off line -- the part most people miss. Since the site is HTTPS, also set AUTH_COOKIE_SECURE=true in /etc/krouter.env so the session cookie is only sent over TLS.

The X-Forwarded-For and X-Real-IP headers are not optional decoration: either one is how kRouter knows a request came through a proxy, and a proxied request is never treated as local. That is what makes the API demand a key and the dashboard refuse the default password.

Step 4: Lock it down

As set up so far, the API needs a key and the dashboard needs your password, but both are on the public internet. Two ways to narrow that.

Option A: Tailscale only

Skip nginx and bind kRouter to the server's Tailscale address instead of loopback:

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
tailscale ip -4   # for example 100.101.102.103

Put that address in the service file (ExecStart=/usr/bin/krouter --skip-update --host 100.101.102.103) and add After=tailscaled.service under [Unit]. Only devices on your tailnet can reach it, and because their requests do not come from loopback, they still need an API key and the real password. Tailscale's free Personal plan covers up to 6 users; a bigger team needs a paid plan.

Do not use the Tailscale option on kRouter's Endpoint page for this. It uses Tailscale Funnel, which publishes the URL to the whole internet.

Option B: Basic auth and an IP allowlist on the dashboard

If you cannot use Tailscale, narrow the dashboard in nginx:

location /dashboard {
    allow 1.2.3.4;       # office IP
    allow 5.6.7.8;       # home IP
    deny all;
 
    auth_basic "kRouter";
    auth_basic_user_file /etc/nginx/.htpasswd;
 
    proxy_pass http://127.0.0.1:20128/dashboard;
}

The /v1 API can stay reachable: remote callers always need a kRouter API key, and they are locked out for a while after 10 bad attempts. The security checklist covers the rest.

Step 5: Team config

Create one API key per person on the dashboard's Endpoint page, so you can switch one off without affecting anyone else. Then each person points their tools at the router:

OPENAI_BASE_URL=https://router.your-domain.com/v1
OPENAI_API_KEY=<their key from the Endpoint page>

Claude Code uses ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN with the same values; the ANTHROPIC_BASE_URL guide has the details.

Everyone hits the same router, the same accounts and the same combos, and the dashboard's Usage page shows the traffic.

A public HTTPS URL is also what Cursor needs, because Cursor sends custom-model requests from its own servers and never reaches localhost. Why Cursor ignores a localhost base URL covers that case. A Tailscale-only router (Option A above) is out of Cursor's reach for the same reason.

Updates

sudo npm install -g @sifxprime/krouter@latest
sudo systemctl restart krouter

Update promptly when a release fixes a security issue; the changelog says when one does. The service runs with --skip-update, so kRouter will not check for updates on its own.

What about Docker?

kRouter publishes images for linux/amd64 and linux/arm64 to Docker Hub and GitHub Container Registry:

KROUTER_PASSWORD="$(openssl rand -base64 18)" && echo "Dashboard password: $KROUTER_PASSWORD"
docker run -d \
  -p 127.0.0.1:20128:20128 \
  -e INITIAL_PASSWORD="${KROUTER_PASSWORD:?run the line above first}" \
  -v "$HOME/.krouter:/app/data" \
  -e DATA_DIR=/app/data \
  --name krouter \
  --restart unless-stopped \
  sifxprime/krouter:latest

Publishing the port on 127.0.0.1 keeps it off the public interface; nginx handles access. The first line prints your dashboard password; INITIAL_PASSWORD is required because logins through nginx are never local. DATA_DIR tells kRouter where to keep its SQLite database inside the container. If you forget the password, the image includes a reset script: docker exec krouter node scripts/reset-password.js.

The database is at $HOME/.krouter/db/data.sqlite on the host. kRouter copies it into db/backups/ before an upgrade, a schema migration or an import, and keeps the last five copies.

Update to the latest image:

docker pull sifxprime/krouter:latest
docker rm -f krouter
# re-run the command above

The same image is at ghcr.io/sifxprime/krouter:latest if you prefer GitHub Container Registry. MITM mode does not work inside a container.

How account routing behaves with a team

With five people on one router, requests arrive in parallel and the account picker matters more. kRouter's default, labelled Fill First in Settings as of v0.5.163, actually runs Zenith: it scores each available account on recent success rate and latency, scales down accounts with under 30% of their tracked quota left, and picks the highest score. The account list and its locks are held in memory, so moving to the next account after a 429 does not wait on the database.

It does not remove rate limits. Accounts still hit them; Zenith just makes it less likely that everyone piles onto the same nearly exhausted one.

RTK with a team

RTK is on by default and compresses the tool output inside agent requests -- diffs, build logs, directory listings, long file reads -- before they go upstream. kRouter's README estimates 20-40% fewer input tokens for agent work; that is an estimate, not a measurement, and it applies per request whether one person or five send them. See how RTK works.

Caveats

  1. Rate limits are per account, not per router. A VPS does not multiply your Kiro free tier. Adding accounts does, with the account-risk caveats covered in multiple accounts per provider.
  2. Streaming needs nginx tuning. Without proxy_buffering off, streamed responses queue up and arrive in chunks.
  3. Credentials live in SQLite, unencrypted. Back up db/ off the server, and protect those backups like SSH keys. The automatic copies only happen at upgrades and imports, so they are not a backup schedule.
  4. MITM is per machine. If a developer needs MITM (Claude Desktop, Antigravity, Kiro IDE or Copilot), they run kRouter on their own laptop. That local kRouter can still use the team router: add it as an OpenAI Compatible provider with base URL https://router.your-domain.com/v1 and their API key.

Install

npm install -g @sifxprime/krouter

The deployment docs cover Docker Compose and the Presidio sidecar as well.

Kodelyth · The team behind kRouter

Published by Kodelyth, the team that builds kRouter. Posts are drafted with AI assistance and reviewed by a person before they go out. kRouter is free and MIT licensed.

Install kRouter