AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Levelsio Vps Deploy

skill-avanderheyde-levelsio-vps-setup-skill-levelsio-vps-setup-skill · by Avanderheyde

Set up a web app on a single cheap, hardened VPS the "levels.io" way — one Hetzner/DigitalOcean box running Next.js (or any Node app) on SQLite + Caddy (auto-HTTPS) + systemd, secured with Tailscale, optionally behind Cloudflare. Offers TWO workflows the user picks between — Path A: develop on your own computer and deploy to the VPS for cheap hosting; Path B (the full levels.io way): develop ON t…

No reviews yet
0 installs
29 views
0.0% view→install

Install

$ agentstack add skill-avanderheyde-levelsio-vps-setup-skill-levelsio-vps-setup-skill

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-avanderheyde-levelsio-vps-setup-skill-levelsio-vps-setup-skill)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Levelsio Vps Deploy? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

levelsio-vps-deploy

Run a web app on one cheap, hardened VPS — the levels.io way. No Kubernetes, no microservices, no managed PaaS. One box, boring tech, vertical scaling, ship fast.

First: pick the workflow (ask the user)

There are two ways to use this skill. Before doing anything, ask the user which they want — they are different setups:

> Do you want to just deploy to the VPS (you develop on your own computer, the > VPS only hosts), or do you want to develop AND host on the VPS itself the way > levels.io does?

| | Path A — host only | Path B — develop on the VPS (the levels way) | |---|---|---| | Where you code | your own laptop | on the VPS, via Claude Code in tmux | | How code reaches prod | rsync + build-on-box (deploy.sh) | edited in place — already live (~3s restart) | | Connect from | — | Termius (laptop and phone) over Tailscale | | Good for | teams, staging boundary, regulated | solo / indie, cheapest always-on dev+host, code from anywhere | | Read | this file's Phase 1 + 2 | references/develop-on-vps.md |

Both share the same hardened foundation below. They are not mutually exclusive — a solo dev can do Path B and still deploy.sh to a separate staging box. When unsure, Path B is "the full levels.io experience"; Path A is "the VPS is just my cheap host."

The stack (defaults — swap only with reason)

| Layer | Choice | Why | |---|---|---| | Host | Hetzner CPX21 (~$8/mo) or DigitalOcean equivalent, Ubuntu 24.04 LTS | flat price, plenty for a small/medium app | | Network/SSH | Tailscale — SSH locked to the tailnet | SSH invisible on the public internet; lets you connect from anywhere incl. phone | | App | Next.js App Router, output: "standalone" (or any Node app) | single self-contained process | | DB | SQLite via better-sqlite3, WAL mode + busy_timeout | one file, no DB server, fast | | Proxy / TLS | Caddy, reverse-proxy to 127.0.0.1:3000 | automatic HTTPS, 3-line config | | Process mgmt | systemd unit, Restart=always | survives crashes and reboots | | Dev (Path B) | Claude Code + tmux, Termius client | persistent on-box development from any device | | Edge (optional) | Cloudflare in front | DNS, origin hiding, WAF, caching | | Backups | Litestream → S3/R2 | continuous offsite SQLite replication |

Vertically scale the box (more RAM/CPU) long before you reach for distribution.

How the pieces fit

        Tailscale (private)                    Internet
   you ── laptop / phone ──┐               (Cloudflare) ↓ :443
                           ▼                            ▼
                    ┌──────────────────────────────────────────┐
                    │  VPS  │ SSH:22 tailnet-only │ Caddy :443  │
                    │       │ Claude Code in tmux │   ↓ :3000   │
                    │       │ (Path B dev)        │ Next.js     │
                    │                               (systemd)    │
                    │                                  ↓          │
                    │                            SQLite (WAL) ──► Litestream → R2
                    └──────────────────────────────────────────┘

Read references/architecture.md for the hosting diagram + rationale, references/security-tailscale.md for the network lockdown, and references/develop-on-vps.md for the on-VPS development workflow.

Shared foundation — Provision + secure (BOTH paths)

Phase 0 — Create the VPS (optional; agent can do this)

If the user doesn't already have a server, create one for them rather than making them click around a cloud console. scripts/create-hetzner-server.sh uses the hcloud CLI to spin up a fresh Ubuntu box with the user's SSH key installed as root — ready for Phase 1. Full walkthrough: references/provision-hetzner.md.

Steps for the agent:

  1. Make sure the user has an SSH key (see "SSH keys" below — generate one if not).
  2. Check hcloud is installed and authenticated: hcloud server list. If not,

guide the user to create a Hetzner API token (Cloud Console → Security → API Tokens, Read & Write) and authenticate themselves so the token never passes through your tool calls — tell them to run, in-session, ! hcloud context create and paste the token at the hidden prompt. The token is a secret (it can create/delete servers); never put it in a script, config, or command line.

  1. Edit the CONFIG block in scripts/create-hetzner-server.sh (SERVER_NAME,

SERVER_TYPE default cpx21, LOCATION, IMAGE, SSH_KEY_PUB) and run it. It prints the new server's IP and the exact Phase 1 command.

  1. Second/Nth server: change SERVER_NAME (and DOMAIN/APP_NAME in

provision.sh) and run again. The script refuses to clobber an existing name. Each box is independent and joins the same tailnet. If the user says "provision another server for ` at `", run this whole chain (create → provision → Tailscale lockdown → their path) for the new box.

If the user already has a box (any provider) with root SSH, skip to Phase 1.

Phase 1 — Provision (one time)

scripts/provision.sh is a template: review it, edit the CONFIG block, run it as root on a fresh VPS. It will:

  1. Create a non-root deploy user with sudo + your SSH key.
  2. Lock down SSH: disable password auth and root login.
  3. Firewall with ufw: allow 22, 80, 443 (22 is locked to the tailnet later

see below — so you don't get locked out mid-provision).

  1. Enable unattended-upgrades and fail2ban.
  2. Install Tailscale (daemon only; you run tailscale up after).
  3. Install Node LTS and Caddy.
  4. Create app dirs (/srv/, /srv/-build, /var/lib/) owned by

deploy, and the secrets file /etc//env (mode 0640, root:deploy).

  1. Write a Caddyfile (DOMAINreverse_proxy 127.0.0.1:3000) and a systemd

unit for the app.

Steps for the agent:

  1. Ask for APP_NAME, DOMAIN, DEPLOY_USER (default deploy), and the VPS IP /

root access.

  1. Set up the SSH key for the user (do this yourself — see "SSH keys" below —

don't make them figure it out). The deploy user must trust a key before SSH is locked down.

  1. Edit the CONFIG block at the top of scripts/provision.sh (paste the public

key from step 2 into DEPLOY_SSH_PUBKEY).

  1. Have the user copy it to the box and run as root:

scp scripts/provision.sh root@SERVER:/tmp/ && ssh root@SERVER 'bash /tmp/provision.sh'.

  1. Generate app secrets server-side (openssl rand -base64 32) into

/etc//env. Transfer third-party creds out-of-band (see Secrets).

SSH keys — generate locally + install on the box (agent-driven)

Do this for the user. A public key is not a secret, so it's safe to read, paste into configs, and echo onto the box. (A private key never leaves the machine it was generated on.)

First device (the user's computer, where you're running):

  1. Check for an existing key:

``bash ls ~/.ssh/id_ed25519.pub 2>/dev/null && cat ~/.ssh/id_ed25519.pub ``

  1. If there's none, generate one (no passphrase keeps phone/Termius login simple;

offer a passphrase + ssh-agent if the user wants more security): ``bash ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "" -C "@" ``

  1. Read the public key (cat ~/.ssh/id_ed25519.pub) and paste it into

DEPLOY_SSH_PUBKEY in provision.sh. For Path A, point SSH_KEY in deploy.sh at the matching private key (~/.ssh/id_ed25519).

Adding another device later (e.g. the user's phone): you can't run commands on their phone, so the phone generates its own key in Termius (Keychain → New Key → Generate). Have the user paste that device's public key to you, then install it on the box yourself:

ssh @ 'umask 077; echo "ssh-ed25519 AAAA...phone..." >> ~/.ssh/authorized_keys'

`` is the tailnet IP once Tailscale is up, otherwise the public IP. No private key is ever copied between devices.

Phase 1b — Tailscale lockdown (recommended, both paths)

This is what levels does on every new box: SSH only over the tailnet. Order matters or you lock yourself out — full detail in references/security-tailscale.md:

  1. On the box: sudo tailscale up (follow the auth link), then tailscale ip -4.
  2. Install Tailscale on your laptop/phone, same account.
  3. Confirm ssh deploy@100.x.y.z works over the tailnet (new terminal, keep

the old session open as a fallback).

  1. Only then: bash scripts/lock-down-tailscale.sh on the box — restricts SSH 22

to the tailnet (and optionally 443 to Cloudflare IPs).

Path A — Deploy from your computer (host only)

scripts/deploy.sh is a template run from your dev machine. The key trick: build on the box so native modules (better-sqlite3) compile for the server. It rsyncs your tree → /srv/-build, runs npm ci && npm run build on the box, assembles the standalone output (copying .next/static, public, and the better-sqlite3 .node binding), swaps it into /srv/, restarts the service, and health-checks the domain.

Steps for the agent:

  1. Confirm the app's next.config.* sets output: "standalone".
  2. Edit the CONFIG block in scripts/deploy.sh (SSH_HOST, SSH_KEY,

APP_NAME). If SSH is locked to the tailnet, SSH_HOST uses the tailnet IP.

  1. Run it. On failure, consult references/troubleshooting.md.

Path B — Develop ON the VPS (the levels.io way)

The full levels workflow: code lives on the box, Claude Code runs in tmux, you connect from laptop or phone via Termius over Tailscale, and edits are basically already live. Read references/develop-on-vps.md for the end-to-end walkthrough.

Setup, after Phase 1 + Tailscale:

  1. On the box: bash scripts/setup-dev-on-vps.sh — installs tmux + Claude Code

and adds the tm "one tmux session per folder" helper to ~/.bashrc.

  1. Put the code where it serves (clone the repo into /srv/ or a per-site

folder like levels' /srv/http/.com).

  1. Log Claude Code in on the headless box: claude/login → use the

browser-link flow (open the link on any device, paste the code back) or an API key. See references/develop-on-vps.md.

  1. Add a Termius host per site with startup snippet cd /srv/ && tm, on both

laptop and phone, pointed at the tailnet IP.

  1. Develop. Detach with Ctrl-b d — Claude keeps running; reattach from any

device. The tmux mechanics are in references/remote-claude-code-tmux.md (also publishable on its own).

Secrets — non-negotiable rules

  • Secrets live only in /etc//env on the server (mode 0640,

owner root:deploy). Never in the repo, the DB, or logs.

  • Generate strong secrets server-side: openssl rand -base64 32.
  • The deploy script / systemd unit sources / loads /etc//env — it never

bakes secrets into the build or prints them.

  • Transfer third-party credentials out-of-band: scp a temp file, extract

into /etc//env, then shred -u the temp file. Never echo a credential on the command line (it lands in shell history and logs).

  • An Anthropic API key used for Path B Claude Code login is a secret too — prefer

the browser-link login, or keep the key out of the repo.

SQLite gotcha (read before first build)

Set busy_timeout before switching the database to WAL mode, or the first write during build/migration can fail with SQLITE_BUSY:

const db = require("better-sqlite3")(process.env.DATABASE_PATH);
db.pragma("busy_timeout = 5000"); // FIRST
db.pragma("journal_mode = WAL");  // THEN

The DB file lives outside the deploy target (e.g. /var/lib//data.db) so rsync --delete never wipes it.

References

  • references/provision-hetzner.md — create the server itself with hcloud

(API token, first + Nth server), plus a DigitalOcean (doctl) alternative.

  • references/architecture.md — hosting diagram, per-layer rationale, scaling,

backups (Litestream → R2).

  • references/security-tailscale.md — Tailscale lockdown, lockout-safe ordering,

recovery.

  • references/develop-on-vps.md — Path B end to end: Termius, Claude Code login,

the live-edit workflow, phone development.

  • references/remote-claude-code-tmux.md — the tmux/persistent-Claude-Code

module (self-contained; also published standalone).

  • references/troubleshooting.mdSQLITE_BUSY, native bindings, Caddy certs,

systemd env, /srv perms, plus Claude-login / tmux / Tailscale-lockout fixes.

Scripts

  • scripts/create-hetzner-server.sh — create the VPS itself via hcloud (Phase 0).
  • scripts/provision.sh — one-time server lockdown + install, incl. Tailscale.
  • scripts/lock-down-tailscale.sh — restrict SSH to the tailnet (+ optional

Cloudflare-only 443). Run after Tailscale is confirmed working.

  • scripts/setup-dev-on-vps.sh — Path B: install Claude Code + tmux + tm helper.
  • scripts/deploy.sh — Path A: repeatable build-on-box deploy.

All use set -euo pipefail, are commented, and have a clearly marked CONFIG block at the top. Review and edit before running.

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.