Skip to content

Latest commit

 

History

46 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vps

One-line bootstrapper that turns a fresh Ubuntu VPS into a secured management box running Cockpit and a single-node k3s/Rancher cluster, with Traefik as a public HTTP/HTTPS ingress. Cockpit, Rancher, and the Traefik dashboard are Tailscale-only; the ingress itself (80/443) is public on purpose - see Security model. This repo also publishes a Helm chart catalog (ArgoCD, Epinio, and anything else added under charts/) that the vps-marketplace step registers in Rancher automatically - see Rancher Marketplace. Every step can be turned back off later without reinstalling anything else - see Removing a feature.

curl -fsSL https://raw.githubusercontent.com/perspikapps/vps/main/setup.sh | sh
zz_use perspikapps/vps/vps-setup
sudo vps-setup

The first two lines bootstrap zz_use (this repo's own package/dependency-fetching mechanism - see One folder per feature) and fetch vps-setup, the interactive/flag-driven orchestrator that runs every step and prints a final connection summary; sudo vps-setup is the one that actually installs anything, and is safe to re-run any time afterwards. See Running vps-setup for the full picture - there is no single one-liner that does everything end-to-end, deliberately: see that section for why.

Run vps-setup with no arguments on an actual terminal and you get an interactive menu instead of having to remember flag names - see Interactive menu. Run with -h for the full flag list (--skip-vps-tailscale, --skip-vps-rancher, etc.), or set env vars beforehand, e.g.:

sudo TAILSCALE_AUTHKEY=tskey-... \
    VPS_ADMIN_USER=ops VPS_ADMIN_SSH_KEY="ssh-ed25519 AAAA..." \
    RANCHER_HOSTNAME=rancher.example.internal \
    vps-setup

Running vps-setup

There's no single command that clones this repo, bootstraps everything, and runs it end-to-end - by design. Every folder here, vps-setup included, is fetched and run the same way any tomgrv/scripts-style package is: zz_use <origin>/<name> then run <name>. Bootstrapping is always the same two steps:

curl -fsSL https://raw.githubusercontent.com/perspikapps/vps/main/setup.sh | sh
zz_use perspikapps/vps/vps-setup

setup.sh only installs zz_use (and the rest of the core zz_* bundle) onto PATH - it's a bulk copy of tomgrv/scripts's own setup.sh, byte-for-byte, kept here only so the one-liner's URL lives under this repo instead of pointing straight at tomgrv/scripts. It never runs anything from this repo itself. zz_use perspikapps/vps/vps-setup is the separate step that actually fetches vps-setup.

From there, sudo vps-setup [flags] is what installs (or removes) anything - see Running a single step below for the full flag reference, and Interactive menu for running it with no flags on a terminal. It's always safe to re-run: every step's script is idempotent, and vps-setup itself re-clones/updates its own working checkout (see below) each time.

Why this needs its own checkout

A single zz_use perspikapps/vps/vps-setup only fetches the vps-setup/ folder - orchestrating every other step means reading every sibling folder's package.json/run.sh, which a single-folder fetch doesn't give you. vps-setup/run.sh handles this itself: run from inside a full local checkout (this repo cloned, vps-setup/run.sh invoked directly), it uses that checkout as-is; run standalone (the zz_use case, which is how the one-liner above works), it clones/updates a full checkout into VPS_SETUP_DIR (default /opt/vps-setup) first, then proceeds exactly the same way. VPS_SETUP_REPO_URL/VPS_SETUP_REPO_REF/ VPS_SETUP_DIR (same env vars as before) control that clone - see Running from a non-standard branch or fork.

Running a single step (or a subset)

vps-setup runs nine feature folders, in the order each one's package.json declares (vps.order - see One folder per feature): vps-system, vps-security, vps-tailscale, vps-cockpit, vps-k3s (includes Traefik configuration), vps-rancher, vps-dockermanager, vps-marketplace, and vps-github-arc. All but vps-github-arc run by default - it's opt-in (see GitHub Actions Runner Controller). Three flag families control which of them run:

  • --skip-<step> - run everything except the named step(s).
  • --with-<step> - turn on an opt-in step that's off by default; harmless (a no-op) on a step that's already on by default, e.g. --with-vps-github-arc.
  • --only-<step> - run only the named step(s), regardless of its default; pass it more than once to run a few together. Any --only-* flag overrides every --skip-*/--with-* flag on the command line.

Once vps-setup is installed (see above), flags are just ordinary arguments - no piped-curl shell-parsing gotchas to worry about:

# Re-run just Rancher, e.g. after changing RANCHER_HOSTNAME:
sudo RANCHER_HOSTNAME=new.example.com vps-setup --only-vps-rancher

# Re-run Cockpit and the dockermanager plugin together, skipping everything else:
sudo vps-setup --only-vps-cockpit --only-vps-dockermanager

# Full run except Rancher (e.g. you're not using Kubernetes on this box):
sudo vps-setup --skip-vps-rancher --skip-vps-k3s

# Re-run just the marketplace catalog registration:
sudo vps-setup --only-vps-marketplace

This is equivalent to (and a convenience wrapper around) invoking a feature's own script directly, as shown in Layout below - --only-vps-rancher just means "run vps-rancher/run.sh through vps-setup's usual repo clone/update, dependency resolution, and final summary, instead of calling it by hand." Because every feature's script is idempotent, re-running a single step to pick up a changed env var (like RANCHER_HOSTNAME above) is safe and won't disturb the others. See -h/--help for the full flag list.

Dependencies between steps

Every feature is its own npm workspace package under <name>/, and its package.json's standard dependencies field is the single source of truth for what it needs - vps-rancher/package.json declares "@tomgrv/vps-k3s": "*", vps-marketplace/package.json declares "@tomgrv/vps-rancher": "*" (Rancher's ClusterRepo API needs Rancher, not k3s directly), and vps-dockermanager/package.json declares "@tomgrv/vps-cockpit": "*" (its plugins are meaningless without Cockpit itself). vps-setup reads that field directly (no separate config to keep in sync) and resolves it transitively: enabling vps-marketplace auto-enables vps-rancher, which in turn auto-enables vps-k3s, even though vps-marketplace/package.json never mentions vps-k3s at all:

# vps-k3s isn't named here, but this still installs it - vps-rancher needs it,
# and vps-marketplace needs vps-rancher:
sudo vps-setup --only-vps-marketplace
# -> [vps-setup] Also enabling 'vps-rancher' (required by 'vps-marketplace').
# -> [vps-setup] Also enabling 'vps-k3s' (required by 'vps-rancher').

The same dependencies field is read in reverse for --down-<step>: bringing vps-k3s down while vps-rancher/vps-marketplace are still enabled is refused, and bringing vps-cockpit down while vps-dockermanager is still enabled is refused too, since either would leave the dependent step broken. Adding a new dependency for a feature is a one-line edit to its package.json - see One folder per feature below.

Interactive menu

Run vps-setup with no arguments, on an actual terminal, to get a menu instead of having to remember flag names:

sudo vps-setup
==== VPS setup menu ====
   1) * vps-system       [up  ] Base system update & essentials
   2) * vps-security     [up  ] Firewall / SSH / fail2ban hardening
   3) * vps-tailscale    [up  ] Tailscale install
   4) * vps-cockpit      [up  ] Cockpit install
   5) * vps-k3s          [up  ] k3s / kubectl / helm install (includes Traefik configuration)
   6) * vps-rancher      [up  ] Rancher install
   7) * vps-dockermanager [up  ] cockpit-packagekit/files/dockermanager install
   8) * vps-marketplace  [up  ] Rancher Apps & Marketplace catalog registration
  (* = installed by default) Enter a number to cycle
  skip -> up -> down -> skip for that step.
  <enter> to proceed, 'q' to quit without changing anything.
>

Type a step's number to cycle it through skip -> up -> down -> skip (down means uninstall it - see the next section), press enter to proceed with whatever you've selected, or q to quit without changing anything. This is purely a friendlier way to build the same --skip-* / --with-* / --down-* selection described above - everything below about flags, env vars, and dependencies applies whether you got there via the menu or the command line.

Removing a feature (up/down per step)

Every step can be brought back down (uninstalled/disabled) independently, without touching anything else already on the box - pass --down-<step> instead of installing it:

# Remove the marketplace catalog registration only (Rancher, k3s,
# Cockpit, etc. are untouched - apps already installed from the
# catalog via Rancher's UI are untouched too):
sudo vps-setup --down-vps-marketplace

# Remove more than one step in the same run:
sudo vps-setup --down-vps-marketplace --down-vps-dockermanager

A step whose dependency is still enabled refuses to come down, so you don't accidentally break something still running:

sudo vps-setup --down-vps-k3s
# [vps-setup] Refusing to bring 'vps-k3s' down: 'vps-rancher' depends on it and is still enabled.
# [vps-setup] Also pass --down-vps-rancher, or --force-down to override (may leave 'vps-rancher' broken).

Either bring the dependent step down in the same run (--down-vps-k3s --down-vps-rancher --down-vps-marketplace, to remove the whole cluster cleanly), or pass --force-down if you really want to pull vps-k3s out from under something still enabled.

What each step's down action actually does - and doesn't - undo:

Step down removes Left in place
vps-system (no down action - a base package upgrade, nothing to undo) everything
vps-security ufw rules (disables ufw entirely), sshd hardening, fail2ban jail the admin user/password up created, if any
vps-tailscale logs out of the tailnet, disables tailscaled the tailscale package itself (PURGE_TAILSCALE=true to remove it too)
vps-cockpit the Cockpit packages and socket config - refused while vps-dockermanager is still enabled -
vps-k3s k3s itself (via its own uninstaller) - takes Rancher, GitHub ARC, and anything installed via the Marketplace down with it -
vps-rancher the Helm release and its namespace cert-manager, apps installed via Apps & Marketplace
vps-dockermanager cockpit-dockermanager, cockpit-packagekit, cockpit-files Docker itself (REMOVE_DOCKER=true to also remove it)
vps-marketplace the ClusterRepo catalog registration only any apps already installed from it (uninstall those from Rancher's UI)
vps-github-arc both Helm releases (controller and runner scale set), the GitHub App secret, and the github namespace -

Each feature's run.sh also accepts the action directly if you'd rather run it without going through vps-setup (e.g. from an existing /opt/vps-setup checkout):

sudo bash vps-marketplace/run.sh down
sudo bash vps-marketplace/run.sh up # same as calling it with no argument

Full copy-paste example

A realistic one-shot install on a fresh Ubuntu VPS, run as root right after first boot. Replace the SSH key and auth key with your own (see Getting the keys you'll need below):

curl -fsSL https://raw.githubusercontent.com/perspikapps/vps/main/setup.sh | sh
zz_use perspikapps/vps/vps-setup

VPS_ADMIN_USER=ops \
    VPS_ADMIN_SSH_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... you@laptop" \
    TAILSCALE_AUTHKEY="tskey-auth-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
    RANCHER_HOSTNAME="rancher.tailnet-name.ts.net" \
    RANCHER_BOOTSTRAP_PASSWORD="$(openssl rand -base64 24)" \
    vps-setup

This creates the ops sudo user with your key, disables SSH password login, joins your tailnet immediately, and installs Cockpit + k3s + Rancher. When it finishes, connect over Tailscale and open Cockpit (https://<tailscale-ip>:9080) and Rancher (https://rancher.tailnet-name.ts.net:7083) from a machine on the same tailnet. Save the printed Rancher bootstrap password (also written to /root/.rancher-bootstrap-password) to log in.

Running from a non-standard branch or fork

Two separate fetches need to agree on the branch/fork you're testing: zz_use's own fetch of the vps-setup folder itself (@<ref> in the zz_use call), and vps-setup/run.sh's internal clone of the full repo (VPS_SETUP_REPO_URL/VPS_SETUP_REPO_REF, since it needs every sibling feature folder too - see Running vps-setup):

BRANCH=claude/vps-setup-ubuntu-scripts-br4ddo

curl -fsSL "https://raw.githubusercontent.com/perspikapps/vps/${BRANCH}/setup.sh" | sh
zz_use "perspikapps/vps/vps-setup@${BRANCH}"

sudo VPS_SETUP_REPO_REF="$BRANCH" vps-setup

Warning

export FOO=bar then sudo vps-setup will NOT pick it up. sudo resets the environment by default, so a plain shell export is invisible to the command it runs - vps-setup will silently fall back to main for its internal clone even though echo $VPS_SETUP_REPO_REF shows the right value in your shell. Either put the assignment directly on the sudo line (as above - it's passed through even with env reset on), or use sudo -E (only works if your sudoers config allows it):

export VPS_SETUP_REPO_REF=my-branch # WRONG on its own: lost by sudo
sudo vps-setup # falls back to main - use sudo -E vps-setup instead

Env vars for the internal clone:

Variable Default Purpose
VPS_SETUP_REPO_URL https://github.com/perspikapps/vps.git Clone a fork instead
VPS_SETUP_REPO_REF main Branch, tag, or commit to check out
VPS_SETUP_DIR /opt/vps-setup Where the repo is cloned/updated

To point at a fork as well as a branch, set both (and fetch vps-setup itself from the fork's ref too):

zz_use "perspikapps/vps/vps-setup@my-feature"
sudo VPS_SETUP_REPO_URL=https://github.com/<you>/vps.git \
    VPS_SETUP_REPO_REF=my-feature \
    vps-setup

vps-setup re-clones into VPS_SETUP_DIR on every run (git fetch + reset --hard if it's already a checkout), so re-running it after pushing new commits to the same branch picks them up automatically - only zz_use's own fetch of vps-setup itself is cached (zz_update forces a fresh download of that, bypassing the cache, if you've changed vps-setup/run.sh itself on the branch you're testing).

Getting the keys you'll need

SSH key pair (for VPS_ADMIN_SSH_KEY) - generate one on your own machine, never on the VPS:

ssh-keygen -t ed25519 -C "you@laptop" -f ~/.ssh/vps_ed25519
cat ~/.ssh/vps_ed25519.pub # paste this whole line as VPS_ADMIN_SSH_KEY
  • If you already have a key, it's usually at ~/.ssh/id_ed25519.pub or ~/.ssh/id_rsa.pub (cat either one).
  • GitHub/GitLab users already have a public key on file: curl https://github.com/<your-username>.keys returns it directly.
  • Docs: GitHub - Generating a new SSH key, Ubuntu - OpenSSH keys.

Tailscale auth key (for TAILSCALE_AUTHKEY) - generate one in the Tailscale admin console:

  1. Go to https://login.tailscale.com/admin/settings/keys.
  2. Click "Generate auth key". For a server, prefer a reusable, ephemeral: off, and pre-approved (if your tailnet requires device approval) key with a short expiry.
  3. Copy the tskey-auth-... value into TAILSCALE_AUTHKEY.

Docs: Tailscale - Auth keys. Without this variable, vps-tailscale/run.sh still installs Tailscale; just run tailscale up manually afterwards and follow the login link.

Rancher bootstrap password (for RANCHER_BOOTSTRAP_PASSWORD) - any string works; generate a random one with:

openssl rand -base64 24

If you don't set it, vps-rancher/run.sh generates and saves one for you automatically.

Provisioning via cloud-init / Kairos

cloud-init/kairos-vps-setup.yaml is a #cloud-config user-data file that bootstraps zz_use and runs vps-setup unattended on first boot - no interactive SSH session needed to kick it off. It works with:

  • Kairos Ubuntu-flavored images, passed as the install config (e.g. kairos-agent install --config kairos-vps-setup.yaml, or via the ISO/PXE/network install config).
  • Any plain cloud-init VPS provider (DigitalOcean, Hetzner Cloud, OpenStack, etc.) that lets you paste "User data" at creation time - Kairos and stock cloud-init share the same document format for the users / write_files / runcmd keys this file uses.

To use it:

  1. Copy the file and fill in the placeholders: your SSH public key (in two places - users[].ssh_authorized_keys and VPS_ADMIN_SSH_KEY), your Tailscale auth key, and RANCHER_HOSTNAME. See Getting the keys you'll need above.
  2. Paste it into your provider's "User data" / cloud-init field (or pass it to Kairos) when creating the VPS.
  3. On first boot the VPS installs itself unattended; check /var/log/vps-setup.log for progress/output.

Because runcmd already executes as root, zz_use/vps-setup need no sudo at all here - see the note in the file itself.

Layout

  • setup.sh - installs zz_use (and the rest of the core zz_* bundle, from tomgrv/scripts) onto PATH, then stops - a bulk copy of tomgrv/scripts's own setup.sh, kept here only so the one-liner's URL lives under this repo. It never runs anything from this repo itself; zz_use perspikapps/vps/vps-setup is the separate next step - see Running vps-setup. Pin the tomgrv/scripts ref with ZZ_ORIGIN_REF (default main), or bootstrap from a fork entirely with ZZ_ORIGIN. See Replicating this pattern in another repo.
  • vps-setup/ - the interactive/flag-driven orchestrator (see Running vps-setup): resolves which steps run (flags, the interactive menu, and dependencies read straight from each package.json), runs each feature's run.sh in order, idempotent and re-runnable, either up or down, then prints the final connection summary (Tailscale URL, Cockpit/Rancher credentials). Like every feature, a top-level <name>/{package.json,run.sh} folder, zz_use-installable on its own (zz_use perspikapps/vps/vps-setup) - but since a single zz_use fetch only pulls this one folder, it clones/updates a full checkout of everything else itself when run standalone (see Running vps-setup). Not an installable step itself, though - its own feature discovery skips it (and vps-common/) explicitly.
  • package.json (root) - an npm workspace root ("workspaces": [<every top-level folder>]); ties every feature package together for tooling (npm install, npm ls, lint-staged, commitlint's workspace-scope rules) without vps-setup itself needing npm/node at all.
  • cloud-init/kairos-vps-setup.yaml - cloud-init/Kairos user-data that bootstraps zz_use and runs vps-setup unattended on first boot.
  • vps-common/ - shared logging/retry/idempotency helpers sourced by every feature's run.sh (strict bash mode, non-interactive apt, "already done" checks); net_port/net_access/all_network_ports for reading each feature's own package.json port declarations - see Network config; and dispatch_action/helm_teardown, the shared plumbing behind every feature's up/down actions. Colors and leveled logging (ok, and zz_log directly for everything else) delegate to zz_colors/zz_log from tomgrv/scripts - the same core shared with tomgrv/devcontainer-features' common-utils feature - bootstrapped on first source via its setup.sh if not already on PATH. This is bash, not POSIX sh - every run.sh (vps-setup included) is a bash script. Like every feature, vps-common/ is a top-level <name>/{package.json,run.sh} folder in this repo, laid out the same way tomgrv/scripts lays out its own scripts - which is what lets zz_use fetch and install it (or any feature) directly from this repo, from anywhere: zz_use perspikapps/vps/vps-common. It isn't an installable step itself, though - feature discovery skips it (and vps-setup/) explicitly.
  • vps-system/ - apt update/upgrade, base tooling, unattended security upgrades.
  • vps-security/ - optional non-root admin user, ufw (default-deny inbound, rules generated from every feature's own package.json port declarations: SSH and Traefik's 80/443 public, everything else Tailscale-only), sshd hardening, fail2ban, and a Cockpit/console login password.
  • vps-tailscale/ - installs Tailscale, enables tailscaled as a systemd service, and joins the tailnet.
  • vps-cockpit/ - installs Cockpit, served on ports 9080/9083.
  • vps-k3s/ - installs k3s (Traefik enabled), kubectl, Helm, and configures Traefik as a public HTTP/HTTPS ingress with a Let's Encrypt certResolver and a Tailscale-only dashboard - see Traefik ingress.
  • vps-rancher/ - installs cert-manager (required by Rancher's self-signed TLS even with ingress disabled) and the latest Rancher via Helm, exposed on ports 7080/7083 through k3s's built-in ServiceLB. Depends on vps-k3s (see its package.json).
  • vps-dockermanager/ - installs cockpit-packagekit, cockpit-files, Docker (docker.io, as a dependency), and the third-party cockpit-dockermanager plugin for managing Docker containers/images from Cockpit. Depends on vps-cockpit (see its package.json) - these are Cockpit plugins, meaningless without Cockpit itself already installed.
  • vps-marketplace/ - registers this repo's Helm chart catalog (charts/, published to GitHub Pages) as a Rancher ClusterRepo, so it shows up under Apps & Marketplace → Repositories - see Rancher Marketplace. Depends on vps-k3s/vps-rancher.
  • vps-github-arc/ - installs GitHub Actions Runner Controller (ARC) via Helm, registering self-hosted runners against a GitHub org/repo - see GitHub Actions Runner Controller. Opt-in (off by default). Depends on vps-k3s.
  • charts/ - Helm charts for "extra" apps (ArgoCD, Epinio) that install onto the k3s cluster rather than the host itself - not a vps-setup feature folder (no run.sh), published as a standard Helm repo and installed through Rancher's UI instead - see Rancher Marketplace.

One folder per feature

Each feature is a small, self-contained npm workspace package:

vps-rancher/
  package.json   # "name": "@tomgrv/vps-rancher" (npm scope + this repo's
                 # own "vps-" prefix - see below), description,
                 # "bin": { "vps-rancher": "run.sh" } (matches the folder),
                 # "vps": { "default": true|false }, "scripts": { "test": "bats test.bats" },
                 # and "dependencies": { "@tomgrv/vps-<other-feature>": "*" }
  run.sh         # up() and down() - see Removing a feature, below
  test.bats      # syntax + shape checks - see Tests, below
  README.md      # this feature's env vars, up/down usage, and dependencies

Every feature folder in this repo follows this shape exactly - README.md and test.bats aren't optional here, even though Replicating this pattern in another repo below lists them as optional for adopting the convention elsewhere.

Folder names carry no ordering (vps-rancher/, not 05-vps-rancher/) - install order is a plain integer, package.json's vps.order, and vps-setup sorts by that instead of by folder name. Everything that used to live in setup.sh's hand-maintained bash tables (label, install order, default on/off, what depends on what) now lives in each feature's own package.json instead:

{
    "name": "@tomgrv/vps-rancher",
    "version": "1.0.0",
    "private": true,
    "description": "Rancher install",
    "bin": { "vps-rancher": "run.sh" },
    "vps": { "order": 5, "default": true },
    "dependencies": { "@tomgrv/vps-k3s": "*", "@tomgrv/vps-common": "*" }
}

"name" is always @tomgrv/<folder> - this repo's own npm scope plus the folder's bare name, which itself always carries the vps- prefix (vps-rancher/, not rancher/) - unlike tomgrv/scripts's own convention of an unscoped "name" identical to the folder, since that repo's scripts aren't all prefixed the same way. "bin" follows the usual {"<folder>": "run.sh"} shape, what makes zz_use perspikapps/vps/vps-rancher resolvable (see Running a single feature via zz_use below) - and what every internal identity (the folder itself, "bin", vps-setup's own flags/state tracking, e.g. --only-vps-rancher) is built from: the package "name" with only the npm scope (@tomgrv/) stripped, never the vps- prefix too. "dependencies" always includes @tomgrv/vps-common (every feature sources it - see Layout), plus any other feature folder it needs, keyed by that folder's own scoped name; vps-setup's own dependency reading (auto-enable, --down-<step> refusal) strips the npm scope the same way and explicitly excludes vps-common/vps-setup from the result, since neither is an installable step, and only counts a key that actually names a real <folder>/{package.json,run.sh} - so an ordinary (non-feature) npm dependency, if this repo ever adds one, won't be mistaken for a step to auto-enable.

Because every folder already carries the vps- prefix uniformly, there's no longer a special case here the way there used to be for vps-tailscale/ alone (its run.sh calls the real tailscale CLI internally, and zz_use has no notion of "bin" at all - it always installs <name>/run.sh under the literal folder name <name> it was asked for - so a folder named plain tailscale/ would have zz_use perspikapps/vps/tailscale shadow the actual binary it depends on). Prefixing every folder the same way sidesteps that class of collision for free, for any feature, not just this one.

Adding a new feature is: create vps-whatever/ with a package.json (following the shape above, with an order that places it where you want in the install sequence), a run.sh (up()/down() + dispatch_action "$@" at the end, same as any other feature - see vps-common/), a test.bats (see any existing feature's for the shape to copy), and a README.md. vps-setup picks it up automatically; add it to root package.json's "workspaces" array too. Removing a feature is deleting its folder (and that array entry).

The root package.json's "workspaces" array registers every feature as an npm workspace member, so standard npm tooling (npm ls, npm install, the repo's existing lint-staged/commitlint config, which already referenced @commitlint/config-workspace-scopes) understands the dependency graph too - package-lock.json resolves vps-rancher's vps-k3s dependency like any other workspace package. vps-setup itself never needs npm installed, though: it reads each package.json's dependencies/vps fields directly with jq (installed on demand via ensure_jq in vps-setup/run.sh if missing, since this can run before vps-system - the step that would otherwise install it - on a totally fresh box).

Running a single feature via zz_use, without this repo at all

Because every feature is a top-level <name>/run.sh folder - the same layout tomgrv/scripts uses for its own scripts - zz_use (from that repo) can fetch and install any one of them directly, from any machine, without cloning this repo or running vps-setup:

curl -fsSL https://raw.githubusercontent.com/tomgrv/scripts/main/setup.sh | sh
command -v jq > /dev/null || sudo apt-get update && sudo apt-get install -y jq # vps-common/run.sh needs it
zz_use perspikapps/vps/vps-rancher
sudo vps-rancher up

zz_use's [org/repo/]<tool>[@ref] syntax resolves perspikapps/vps as the origin and vps-rancher as the script, downloads this repo (cached locally after the first call, per-origin/ref - see tomgrv/scripts's README), and symlinks vps-rancher/run.sh onto PATH as vps-rancher. Since every feature's own run.sh in turn fetches vps-common/run.sh from this same repo the same way, a feature installed this way works exactly like it would through vps-setup - it just skips discovery, ordering, dependency auto-enable, and the interactive menu, so you're responsible for running any features it depends on yourself first (see Dependencies between steps).

Network config (each feature's own package.json)

Every port this repo opens, and whether it's public or Tailscale-only, is declared on the feature that owns it, in its package.json's vps.ports array (same file that carries vps.default/dependencies - see One folder per feature). Each entry looks like:

{
    "name": "rancher_http",
    "port": 7080,
    "access": "tailscale",
    "note": "optional, becomes the ufw rule's comment"
}

(access is "tailscale" or "public".) vps-rancher/package.json carries rancher_http/rancher_https, vps-k3s/package.json carries http/https/traefik_dashboard, and so on - each feature's own run.sh is what actually binds the port, so its declaration lives right next to the code that uses it instead of a separate central file.

vps-security/run.sh doesn't know about any of that port detail itself: it calls vps-common/run.sh's all_network_ports(), which scans every */package.json and builds ufw's rules from whatever it finds - there's no per-service ufw logic in that script at all, just a loop over that combined list. Every feature that binds a port itself (Cockpit, Rancher, Traefik's dashboard) reads its own default via vps-common/run.sh's net_port() helper (resolving its own package.json automatically - see the function's comment for how vps-setup/run.sh, which isn't any one feature, asks for another feature's port explicitly), so the port ufw opens and the port the app actually listens on can't drift apart.

To change a default port for good, edit that feature's package.json and re-run the affected step(s) (e.g. --only-vps-security --only-vps-rancher after changing rancher_http). To override a port for a single run without editing anything, use its env var - the name is always the entry's name, upper-cased, with _PORT appended: rancher_http -> RANCHER_HTTP_PORT, ssh -> SSH_PORT, and so on.

Lookups are done with jq (already a base dependency installed by vps-system) - no separate YAML tooling needed now that this lives in package.json alongside everything else npm already parses.

Security model

SSH and HTTP/HTTPS (Traefik's ingress) are the only things reachable from the public internet. Everything else - Cockpit, Rancher, the Traefik dashboard, the k3s API - is bound by ufw to the tailscale0 interface only, so you must join the same tailnet to reach them. Apps you install through Rancher's Apps & Marketplace (ArgoCD, Epinio, or anything else) manage their own exposure - see each chart's README.md under charts/ and Rancher Marketplace for what to check before installing one.

HTTP/HTTPS are public unconditionally, not behind a flag: Traefik is this VPS's real ingress, and Let's Encrypt's HTTP-01 challenge needs port 80 reachable from the internet to issue certs at all - a Tailscale-only ingress would defeat the point of having one.

Because of this, vps-setup refuses to run at all if the Tailscale step is enabled but TAILSCALE_AUTHKEY is unset - proceeding anyway would lock down ufw and leave every Tailscale-only service unreachable by anything. Pass --skip-vps-tailscale if you genuinely want to run without Tailscale (you can join manually later with tailscale up, then sudo vps-setup --only-vps-tailscale).

Traefik ingress: Let's Encrypt and the dashboard

vps-k3s/run.sh leaves k3s's bundled Traefik enabled (rather than disabling it, as you'll see suggested in some k3s+Rancher guides) and configures it as this VPS's public ingress via a HelmChartConfig - k3s's own mechanism for overriding a bundled chart's values, watched continuously so it's safe to re-apply any time (e.g. via --only-vps-k3s).

  • Public HTTP/HTTPS, any hostname: ports 80/443 are k3s's own defaults for Traefik's web/websecure entrypoints, exposed via its built-in ServiceLB like Rancher's ports - no extra configuration needed, just ufw open on those two (see Security model). Traefik routes by the incoming request's Host header, not a fixed hostname list: any FQDN or subdomain you point at this VPS's public IP is routed by whichever Ingress declares that host, with no changes needed here
    • that's how a Marketplace-installed app's per-app subdomains would work too, if it uses one (e.g. Epinio - see Rancher Marketplace).
  • Let's Encrypt: a certResolver named letsencrypt is configured (email from TRAEFIK_ACME_EMAIL, HTTP-01 challenge on the web entrypoint, state persisted to a PVC so certs survive pod restarts). This makes the resolver available - it doesn't issue anything by itself. To get a real cert for your own app, create an Ingress (or Traefik IngressRoute) with the annotation traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt, and a real DNS record pointing this VPS's public IP at your hostname (the HTTP-01 challenge needs that to succeed). Each hostname gets its own cert, issued on demand the first time it's requested - HTTP-01 can't issue a single wildcard cert covering a domain and all its subdomains at once (that needs a DNS-01 challenge, which isn't wired up here); every Ingress you add gets its own cert instead.
  • Staging by default: TRAEFIK_ACME_STAGING defaults to true, which points the resolver at Let's Encrypt's staging environment - browsers will show a certificate-warning page, but there's no rate limit, so it's safe to test against repeatedly while you get your Ingress/DNS right. Set TRAEFIK_ACME_STAGING=false once you're ready for real, trusted certs (production Let's Encrypt has strict per-domain rate limits - avoid iterating against it directly).
  • Dashboard: exposed on TRAEFIK_DASHBOARD_PORT (default 8088), Tailscale-only like Cockpit/Rancher, at http://<tailscale-ip>:8088/dashboard/ (trailing slash required). It has no login of its own - that's fine given it's already gated to the tailnet, same threat model as the rest of this repo's admin surfaces, but don't put it on a public port.

Cockpit and Rancher logins

  • Cockpit authenticates via PAM against a real Linux account and password - separate from SSH, which stays key-only. vps-security/run.sh sets a password for VPS_ADMIN_USER (or root if that's unset): either VPS_ADMIN_PASSWORD if you set it, or a random one saved to /root/.cockpit-admin-password (username in /root/.cockpit-admin-user).
  • Rancher username is always admin; the initial password is RANCHER_BOOTSTRAP_PASSWORD if set, otherwise a random one saved to /root/.rancher-bootstrap-password. Rancher prompts you to change it on first login.

Anything you install afterwards through Apps & Marketplace (ArgoCD, Epinio, etc.) sets up its own login the way its own chart does - see Rancher Marketplace and that chart's README.md under charts/.

Rancher Marketplace

"Extra", non-essential apps - things that run on the k3s cluster rather than being part of the host-level bootstrap - aren't installed by vps-setup any more. Instead, this repo publishes them as a standard Helm chart repo from charts/, and the vps-marketplace step (on by default) registers that repo as a Rancher ClusterRepo so it shows up under Apps & Marketplace → Repositories as perspikapps-vps, pointed at https://perspikapps.github.io/vps/.

From there, installing (or removing) ArgoCD, Epinio, or anything else this repo publishes is just using Rancher's own Apps & Marketplace → Charts UI like any other catalog app - fill in that chart's values (see its README.md under charts/<name>/ for what's required) and install. vps-setup itself no longer knows how to install/uninstall these apps directly; vps-marketplace/run.sh down only removes the catalog registration; uninstall an already-installed app from Rancher's UI.

Why this split: vps-cockpit/ and vps-dockermanager/ stay as vps-setup steps because they configure the host itself (apt packages, systemd services) - a Helm chart doesn't fit them. ArgoCD and Epinio, by contrast, are ordinary Kubernetes workloads with nothing VPS-specific about them once installed, so a Rancher-native catalog is a better fit than a bash script re-running helm upgrade --install - it gets you Rancher's own install/upgrade/values UI, version pinning, and easy removal for free.

Publishing: charts/<name>/ are thin umbrella charts (a Chart.yaml dependency pointing at the real upstream chart, plus a values.yaml with sane defaults) - see charts/argocd and charts/epinio. .github/workflows/publish-charts.yml packages every chart under charts/* and publishes them (via helm/chart-releaser-action) as GitHub Releases plus an index.yaml on the gh-pages branch, whenever charts/** changes on main - that gh-pages branch, served via GitHub Pages, is what https://perspikapps.github.io/vps/ actually serves. Adding a new app to the catalog is: add charts/<name>/Chart.yaml + values.yaml (+ README.md documenting any required values), push to main, and it's live in the catalog within a few minutes.

Cockpit/Rancher's own credentials are still printed by vps-setup/run.sh at the end of an install; anything installed through the Marketplace prints its own credentials/URLs the way that chart's own notes (or its README.md under charts/) describe.

GitHub Actions Runner Controller (ARC)

Opt-in - pass --with-vps-github-arc (or --only-vps-github-arc) to install it; it doesn't run on a plain vps-setup with no flags.

vps-github-arc/run.sh installs GitHub Actions Runner Controller (ARC) via its two official Helm charts - the controller (gha-runner-scale-set-controller) and a runner scale set (gha-runner-scale-set) - both into a single github namespace on the k3s cluster, so self-hosted GitHub Actions runners can be dispatched straight onto this VPS. Depends on vps-k3s (see its package.json).

Authentication is via a GitHub App, the method the docs recommend over a personal access token - you create the App yourself (following the quickstart above) and give this script its credentials; it doesn't create the App for you.

  • Required: GITHUB_ARC_CONFIG_URL (the org or repo the runners register against, e.g. https://github.com/perspikapps or https://github.com/perspikapps/vps), GITHUB_ARC_APP_ID, GITHUB_ARC_APP_INSTALLATION_ID, and GITHUB_ARC_APP_PRIVATE_KEY_FILE (a path to the App's private key PEM
    • not the key content itself, so it's never passed on the command line or logged).
  • Scaling: GITHUB_ARC_MIN_RUNNERS/GITHUB_ARC_MAX_RUNNERS (default 0/5) control the runner scale set's autoscaling range.
  • Neither chart binds a port ufw needs to know about: runners connect outbound to GitHub, nothing needs to be reachable from outside the cluster.
sudo GITHUB_ARC_CONFIG_URL=https://github.com/perspikapps/vps \
    GITHUB_ARC_APP_ID=123456 \
    GITHUB_ARC_APP_INSTALLATION_ID=78901234 \
    GITHUB_ARC_APP_PRIVATE_KEY_FILE=/root/github-arc-app.private-key.pem \
    vps-setup --only-vps-github-arc

Check on it with kubectl -n github get autoscalingrunnersets and kubectl -n github get pods; vps-setup reports whether it's installed like every other step.

Key environment variables

All *_PORT variables below are per-run overrides of a default that actually lives in the owning feature's own package.json - see Network config - edit that file to change a default for good, or set the env var for one run.

Variable Default Purpose
VPS_ADMIN_USER unset Create this sudo user
VPS_ADMIN_SSH_KEY unset Authorized key for the admin user and root
VPS_ADMIN_PASSWORD random Cockpit/console login password (separate from SSH)
SSH_PORT 22 SSH port kept open publicly
TAILSCALE_AUTHKEY unset Auto-join a tailnet (required unless --skip-vps-tailscale)
TAILSCALE_EXTRA_ARGS unset Extra flags appended to tailscale up
COCKPIT_HTTP_PORT / COCKPIT_HTTPS_PORT 9080 / 9083 Cockpit ports (9xxx)
RANCHER_HTTP_PORT / RANCHER_HTTPS_PORT 7080 / 7083 Rancher ports (7xxx)
RANCHER_HOSTNAME node IP Hostname used in Rancher's cert
RANCHER_BOOTSTRAP_PASSWORD random Rancher initial admin password
INSTALL_DOCKER true Install docker.io for cockpit-dockermanager to manage
COCKPIT_DOCKERMANAGER_VERSION latest cockpit-dockermanager release tag to install
TRAEFIK_ACME_EMAIL placeholder Let's Encrypt contact email - set this to a real address
TRAEFIK_ACME_STAGING true Use Let's Encrypt's staging (untrusted, no rate limit) vs. production certs
TRAEFIK_DASHBOARD_PORT 8088 Traefik dashboard port (Tailscale-only)
MARKETPLACE_REPO_NAME perspikapps-vps Name of the Rancher ClusterRepo the vps-marketplace step registers
GITHUB_ARC_CONFIG_URL unset Org/repo URL runners register against (required to run vps-github-arc)
GITHUB_ARC_APP_ID unset GitHub App ID (required to run vps-github-arc)
GITHUB_ARC_APP_INSTALLATION_ID unset GitHub App installation ID (required to run vps-github-arc)
GITHUB_ARC_APP_PRIVATE_KEY_FILE unset Path to the GitHub App's private key PEM (required to run vps-github-arc)
GITHUB_ARC_MIN_RUNNERS / GITHUB_ARC_MAX_RUNNERS 0 / 5 Runner scale set autoscaling range
MARKETPLACE_REPO_URL https://perspikapps.github.io/vps/ URL of the Helm chart catalog to register
CERT_MANAGER_VERSION latest Pin cert-manager's chart version (installed by vps-rancher)

Ports follow a per-app range so they're easy to tell apart at a glance: Cockpit 9xxx, Rancher 7xxx, Traefik dashboard 8xxx - the ingress itself is always 80/443, per HTTP/HTTPS convention, not part of this scheme. Apps installed through the Rancher Marketplace (see Rancher Marketplace) configure their own ports/ Ingresses via that chart's own values, outside this table.

Each feature's run.sh can also be run standalone, from within a checkout or on its own - but it doesn't bootstrap zz_use itself (that's setup.sh's job, run once - see Layout); it just fetches vps-common/run.sh from this repo via zz_use perspikapps/vps/vps-common if zz_use is already on PATH, and fails fast with a one-line message pointing at setup.sh if it isn't:

curl -fsSL https://raw.githubusercontent.com/perspikapps/vps/main/setup.sh | sh
sudo RANCHER_HOSTNAME=new.example.com bash vps-rancher/run.sh

This is what vps-setup --only-<step>, described in Running a single step (or a subset) above, does for you.

Every run.sh also takes an explicit up or down action as its first argument (up is the default, so the invocation above is really ... bash vps-rancher/run.sh up) - see Removing a feature for what each step's down does.

Troubleshooting: a step fails or "just stops"

Every script runs under set -euo pipefail and sources vps-common/run.sh, which installs an error trap: the first command that fails without being explicitly handled (i.e. not part of an if/&&/||) prints its exact file, line number, and the failing command, then the script exits. For example:

[vps-setup] ERROR: command failed (exit 1) at /opt/vps-setup/vps-rancher/run.sh line 52: helm upgrade --install rancher ...

When a step fails during a full vps-setup run, it also prints which numbered step failed and how to re-run just that one after fixing the issue:

[vps-setup] Step 'Rancher install' (vps-rancher/run.sh up) failed (exit 1) - see the error above. Fix it and re-run just this step with: sudo vps-setup --only-vps-rancher

If you ever see a step stop with truly no output at all (not even its own first log line), that most often means a prerequisite step was skipped - e.g. running --only-vps-rancher on a box where --only-vps-k3s (or a full run) was never done first, so kubectl/helm don't exist yet. vps-rancher/run.sh and vps-marketplace/run.sh both check for kubectl (and vps-rancher/run.sh for helm) explicitly and die with a clear message in that case; if you hit a silent stop anywhere else, please open an issue with the exact command you ran and the last few lines of output.

Tests

npm install --global bats # or: apt-get install bats
npm test # or: bats --recursive .

Following tomgrv/scripts's own convention, every feature folder carries its own <name>/test.bats (runnable on its own with bats <name>/test.bats, or via that folder's npm test), covering that folder's run.sh syntax (bash -n), its zz_use/vps-common wiring, up()/(where applicable) down(), and its package.json's bin/vps.order/dependencies shape. vps-common/test.bats additionally covers vps-common/run.sh's pure logic (net_port, net_access, all_network_ports, feature_package_json, dispatch_action) against a small fixture tree. tests/ holds only what doesn't belong to any single folder: setup.sh itself, and invariants spanning every folder's run.sh/package.json (e.g. no leftover log/warn/die wrappers - see tests/test-syntax.bats). The features themselves (apt/Helm/k3s installs) need a live root Ubuntu box to actually test, so that part of this repo has no automated coverage.

Replicating this pattern in another repo

This repo, tomgrv/devcontainer-features' common-utils feature, and tomgrv/scripts itself all share the same shape - a repo that's both a normal codebase and a zz_use-installable source of scripts. Adopting it elsewhere:

  1. One top-level folder per script, each an npm workspace package: <name>/package.json + <name>/run.sh (+ optionally README.md, test.bats, config/). This is the one hard requirement - zz_use org/repo/<name> only works if <name>/run.sh sits directly under the repo root. package.json needs at minimum a "name" and "bin": {"<name>": "run.sh"} (the latter is for npm/workspace tooling only - zz_use itself always installs under the literal folder name requested, never reads "bin" - see One folder per feature above for why that distinction matters, e.g. vps-tailscale/).
  2. A root setup.sh that installs zz_use (and the core zz_* bundle, from tomgrv/scripts) onto PATH, then stops - a bulk copy of tomgrv/scripts's own setup.sh (see Layout), kept in your repo only so the one-liner's URL lives under it instead of pointing straight at tomgrv/scripts. It doesn't hardcode perspikapps/vps anywhere (or anything else about this repo) - copy it verbatim, unmodified. If you need something that runs every script in sequence (this repo's vps-setup, described throughout this README), that's its own ordinary <name>/{package.json,run.sh} folder like any other - fetched and run as its own explicit step (zz_use <org>/<repo>/<name>, then run <name>), never auto-exec'd by setup.sh itself. Individual scripts don't bootstrap zz_use themselves - that would mean one curl per script instead of one total, exactly the duplication a root setup.sh exists to avoid. They just fail fast if it's somehow still missing (e.g. run standalone, before setup.sh):
    command -v zz_use > /dev/null 2>&1 || {
        echo "zz_use not found on PATH - run this repo's setup.sh first: curl -fsSL https://raw.githubusercontent.com/<org>/<repo>/main/setup.sh | sh" >&2
        exit 1
    }
    Never embed the tomgrv/scripts URL directly in more than one place.
  3. A shared common/ folder (or whatever you'd call it) for logic more than one script needs - not a "core" script itself, just another <name>/run.sh folder, sourced via zz_use <org>/<repo>/common; . common rather than a relative source ../lib/common.sh, so it resolves the same way whether a script runs from a local checkout, standalone, or zz_use-installed from anywhere. Exclude it (and anything else that's shared logic rather than an installable unit, like this repo's orchestrator, vps-setup/) from whatever discovers your installable units by convention - see vps-setup/run.sh's own list_feature_dirs()/ feature_deps() here for how this repo does it.
  4. Root package.json: an npm workspaces root listing every folder explicitly (not a glob - see tomgrv/scripts's own package.json for the same convention), so npm install/npm ls understand the whole graph and zz_use-resolvable folders that reference each other as real "dependencies" (@<org>/<repo>-<name> here) actually work.
  5. Tests: sh -n/bash -n every script at minimum; bats for anything with pure logic worth covering (see tests/ here). Anything that genuinely needs a live target system (this repo's own apt/Helm/k3s installs) won't have automated coverage from within the repo alone - say so rather than skipping the question.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages