Single-Node VPS Deployment¶
This runbook prepares the early-access swarmhosts.com control plane on one VPS
running a single-node k3s cluster.
The game servers still run on registered swarm host machines. The VPS hosts the Flask control plane, Kubernetes ingress, app state, background workers, and the operator tooling needed to build and roll out the web image.
What This Provides¶
- k3s single-node Kubernetes with the bundled Traefik ingress controller.
- Traefik ACME HTTP-01 certificate issuance for
https://swarmhosts.com. - A VPS Kustomize overlay at
deploy/kustomize/swarmhosts-single-node-vps. - Docker on the deploy host so
scripts/release/deploy_web.shcan build and push the web image. - A helper script for the registry pull/push secret expected by the deployment and release script.
Initial Deployment Order¶
- Provision the VPS with an Ubuntu or Debian image and SSH access.
- Copy the Ansible examples and fill in the real host and operator values:
cp infra/ansible/inventories/single-node-vps.example.ini \
infra/ansible/inventories/single-node-vps.ini
cp infra/ansible/group_vars/swarmhosts_vps.example.yml \
infra/ansible/group_vars/swarmhosts_vps.yml
The private copies are ignored by git.
- Update
infra/ansible/inventories/single-node-vps.iniwith the VPS IP and SSH user. - Update
infra/ansible/group_vars/swarmhosts_vps.ymlwith: swarmhosts_deploy_userswarmhosts_letsencrypt_emailswarmhosts_repo_url, if the playbook should clone the repo- an explicit
swarmhosts_k3s_version, once the first server is proven - Bootstrap the node:
ansible-playbook \
-i infra/ansible/inventories/single-node-vps.ini \
infra/ansible/playbooks/single-node-vps.yml
- Clone or update the repo on the VPS at
swarmhosts_repo_path. - Create a private
.envon the VPS deploy checkout. At minimum, set: APP_SECRET_KEYFILE_GATEWAY_TOKEN_SECRETRELAY_ROUTE_LEASE_SECRETSWARM_RELAY_REGISTRATION_TOKEN, when platform relay nodes will registerLETSENCRYPT_EMAILBOOTSTRAP_ADMIN_EMAILSMAIL_*settings when email verification/password reset should send mailDNS_*settings only when swarm host DNS automation is readyDOCKER_HUB_*settings for the private web app image repository and, if enabled, managed game version sync- Create the registry secret in the k3s cluster:
REGISTRY_URL=docker.io \
REGISTRY_USERNAME=<docker-hub-user-or-namespace> \
REGISTRY_PASSWORD=<docker-hub-pat> \
KUBECONFIG=/etc/rancher/k3s/k3s.yaml \
KUBECTL=/usr/local/bin/kubectl \
scripts/release/create_web_registry_secret.sh
- Point DNS at the VPS:
swarmhosts.com A <vps-ipv4>swarmhosts.com AAAA <vps-ipv6>, if IPv6 is configured and firewalled- leave
image.swarmhosts.compointed at the onsite registry unless that registry is deliberately migrated; the production web app no longer depends on it for normal pulls - Deploy the app:
KUSTOMIZE_DIR=deploy/kustomize/swarmhosts-single-node-vps \
KUBECONFIG=/etc/rancher/k3s/k3s.yaml \
KUBECTL=/usr/local/bin/kubectl \
./scripts/release/deploy_web.sh
From another workstation, use the location-aware wrapper after the local commit has been pushed:
./scripts/release/deploy_web_anywhere.sh
The default wrapper target is ubuntu@swarmhosts-vps-1 and
/home/ubuntu/repos/ai-agent-swarm-hosts. It streams the pushed commit as an
archive, so the VPS checkout does not need GitHub credentials.
- Verify:
/usr/local/bin/kubectl rollout status deployment/swarmhosts-webserver \
-n default --timeout=180s
curl -fsS https://swarmhosts.com/healthz
- After the control plane is stable, add self-hosted website analytics with
Umami. Keep this as a second rollout so app launch, DNS, TLS, and app
persistence can be verified before adding another database-backed service.
Point
analytics.swarmhosts.comat the VPS, then create the Umami secret and apply the analytics manifests:
KUBECONFIG=/etc/rancher/k3s/k3s.yaml \
KUBECTL=/usr/local/bin/kubectl \
scripts/release/create_umami_secret.sh
/usr/local/bin/kubectl apply -k deploy/kustomize/umami
/usr/local/bin/kubectl rollout status statefulset/swarmhosts-umami-postgres \
-n default --timeout=180s
/usr/local/bin/kubectl rollout status deployment/swarmhosts-umami \
-n default --timeout=180s
Log in to Umami, create the swarmhosts.com website, copy its website ID into
the deploy-host .env, and redeploy the web app:
ANALYTICS_PROVIDER=umami
ANALYTICS_SCRIPT_URL=https://analytics.swarmhosts.com/script.js
ANALYTICS_WEBSITE_ID=<umami-site-id>
ANALYTICS_DOMAINS=swarmhosts.com
ANALYTICS_TRACK_AUTHENTICATED=1
Deployment Considerations¶
DNS¶
The app ingress only routes swarmhosts.com today. Add www.swarmhosts.com
later if a public redirect is desired. Do not point *.swarmhosts.com at the
VPS unless that wildcard is intentionally reserved for a control-plane-owned
feature; swarm host gateway DNS records should continue to resolve to the
swarm host addresses they represent.
Traefik needs port 80 reachable for ACME HTTP-01 challenges, even though normal traffic redirects to HTTPS.
TLS¶
The single-node overlay adds a Traefik certResolver named letsencrypt to the
existing IngressRoute. The Ansible playbook configures that resolver through
k3s' HelmChartConfig.
Certificate issuance will not complete until public DNS points at the VPS and ports 80/443 are open.
Secrets¶
Keep runtime secrets out of git. The release script syncs the optional
swarmhosts-web-secret from the private .env on the deploy host and preserves
existing generated secrets when they are already present.
Important stable secrets:
APP_SECRET_KEYsigns Flask sessions and CSRF state.FILE_GATEWAY_TOKEN_SECRETsigns file-gateway sessions used by swarm hosts.RELAY_ROUTE_LEASE_SECRETsigns relay route leases.SWARM_RELAY_REGISTRATION_TOKENauthorizes system-owned relay nodes to register with the control plane.BOOTSTRAP_ADMIN_EMAILSshould be explicit before early access opens.
Registry¶
The deployment references swarmhost/swarmhosts-app. The harbor-credentials
secret name is retained for compatibility, but its registry value should be
docker.io and its credentials should be Docker Hub credentials that can push
and pull the private app repository. The helper script creates the combined
image pull and deploy-script login secret shape.
Migrating the registry itself to the VPS is a separate infrastructure change.
Persistence¶
The control plane uses the in-cluster swarmhosts-postgres PostgreSQL
StatefulSet for relational state. The app still mounts the swarmhosts-web-data
PVC at /data for uploads, backup archives, file bundles, logs, and retained
SQLite rollback/reference data at /data/swarm.db.
For early access, set up a VPS-level snapshot or file backup before inviting users. A single-node local-path PVC is simple, but it is tied to that node and is not highly available.
Public Probes¶
Public reachability checks run from the swarmhosts-public-probe-worker
Deployment in deploy/kustomize/swarmhosts. The worker uses the control-plane
image with PUBLIC_PROBE_RUNNER=local, so checks originate from the public
cluster network without SSHing from the Flask web process.
Keep one worker replica for early access. If the control plane moves to a multi-node or HA database topology, either move PostgreSQL to managed HA storage or replace direct worker DB writes with an authenticated probe-result API.
The legacy SSH probe mode still exists for lab use by setting
PUBLIC_PROBE_SSH_HOST, but it is no longer the preferred VPS production
shape. Avoid root passwords for any long-lived SSH probe fallback.
Website Analytics¶
Use Umami for the first self-hosted analytics pass. It is a better early-access fit than Plausible Community Edition or Matomo because it keeps the stack small: one web app plus PostgreSQL, no ClickHouse, no PHP/MySQL surface, and no paid cloud dependency.
Implemented shape:
- host analytics at
analytics.swarmhosts.com - deploy Umami in k3s under the separate
deploy/kustomize/umamiKustomize entrypoint - use a PostgreSQL PVC sized around
10Gito start - use a separate
5Gibackup PVC for daily PostgreSQL dumps - expose Umami through Traefik on the same
websecureentrypoint and cluster certificate resolver - store
DATABASE_URL,APP_SECRET, and PostgreSQL credentials in a Kubernetes secret - run a daily
pg_dumpbackupCronJobwith 14-day retention pruning - wire the Flask app with
ANALYTICS_PROVIDER=umami,ANALYTICS_SCRIPT_URL=https://analytics.swarmhosts.com/script.js, andANALYTICS_WEBSITE_ID=<umami-site-id> - include authenticated product traffic without sending user identifiers by
setting
ANALYTICS_TRACK_AUTHENTICATED=1
Track only coarse product events that answer launch questions:
start_free_clickeduser_login_submitteduser_registration_submittedgame_selecteddeployment_create_submittedagent_downloaded
Do not send emails, auth tokens, IP addresses, deployment IDs, swarm host IDs, server names, file paths, logs, user-entered config values, or any other freeform user data to analytics.
The browser analytics helper only forwards an explicit event allowlist and a
small property allowlist (game_id, role, source, status, surface).
Keys that look like secrets, email, addresses, IDs, names, paths, or logs are
dropped client-side before an event reaches Umami.
Sizing¶
For early access, start with enough CPU and RAM headroom for image builds and app workers: at least 2 vCPU and 4 GiB RAM is a practical floor.
For a 100 GB VPS disk, keep an explicit budget:
- OS, k3s, and system logs: 10-15 GB
- Docker image layers and build cache: 20-30 GB
- Swarm Hosts app PVC: 20 GB
- Umami PostgreSQL PVC: 10-15 GB
- backups, logs, and emergency headroom: 20 GB or more
The highest disk risk is accumulated Docker build cache and backups, not Umami itself. Add pruning or alerts before opening early access broadly.
Security¶
Keep the Kubernetes API private to the VPS unless there is a clear need to manage it remotely. The bootstrap playbook opens SSH, HTTP, and HTTPS only. Harden SSH separately with keys, disabled password login, and provider firewall rules once access is confirmed.
Rollback¶
The release script tags each web image immutably. For a bad app rollout, use
Kubernetes rollout history or set the deployment image back to the previous
swarmhost/swarmhosts-app:<tag>. Before risky migrations, capture a copy of
/data/swarm.db, the /data tree, and a PostgreSQL dump from
swarmhosts-postgres.