Files
vpsmgr/AGENTS.md
T
bluebluesodaandCommandCodeBot 4c93ae4be3 feat(haproxy): replace Traefik with a pinned HAProxy domain proxy
The panel now fronts user domains with HAProxy 3.4 LTS (official performance
packages) instead of Traefik: about 100 MB of RSS and 200 MB of disk saved for
a feature set that never used more than Host routing on :80 and SNI passthrough
on :443.

- hpx: publish the proxy configuration as immutable GENERATIONS. Every domain
  mutation renders the whole set from the DB, validates it with the pinned
  `haproxy -c`, and only then repoints the `current` symlink, so a rejected
  candidate leaves the running proxy untouched and a stale domain can never
  survive into a later generation. Routing is carried by two map files
  (Host -> backend, SNI -> backend): per-domain frontend rules cannot work from
  a second file because HAProxy rejects duplicate section names since 3.3 and
  attaches bare directives to the section opened last.
- mgr: republish and reload (SIGUSR2, i.e. `systemctl reload`) after every
  domain change, so a change takes effect without dropping established HTTP or
  TLS connections. Rollback semantics (DB first, then publish) are unchanged.
- cfg,db: `net.traefik` is renamed to `net.haproxy` with identical values. A
  pre-rename config.yaml key and the legacy VPSMGR_TRAEFIK env var are still
  read (new name wins), and schema migration v18 moves the settings mirror from
  `traefik` to `haproxy`, keeping a newer value when both rows exist.
- 30-haproxy.sh: install the pinned branch from the vendor repository and
  migrate a Traefik host in one run — install and validate HAProxy first, then
  remove Traefik's binary, unit, service account and dynamic rules (only its
  config file is kept, inert). No fallback keeps both proxies alive.
- upgrade-haproxy.sh: follow the pinned branch to its newest patch, with a
  configuration check and a graceful reload; `install.sh --update` runs it.
- tests: generation publish/rollback, map rendering, config-key and settings
  migrations, plus real-binary tests (skipped unless
  VPSMGR_TEST_HAPROXY_BIN is set) that run the generated configuration through
  `haproxy -c` and start it in a network namespace.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-09-16 12:59:13 +00:00

4.9 KiB

AGENTS.md

Guide for AI coding agents working in this repo.

Project

vpsmgr — a lightweight LXC hosting panel. Debian 13/Ubuntu host + Incus containers, one Debian 13 container per user, managed via a web panel and a CLI (single Go binary). Hosting-panel concepts, not an ordinary web app: installers, containers, networking, storage.

Commands (always set CGO_ENABLED=0)

The project is pure Go — no cgo, never enable it. net (stdlib) pulls in runtime/cgo whenever a C toolchain is present, which fails on machines with broken gcc.

cd src
CGO_ENABLED=0 go build ./...
CGO_ENABLED=0 go test ./...
CGO_ENABLED=0 go vet ./...

Shell scripts: check syntax with bash -n scripts/*.sh. Note: install.sh and 00-ip-ask.sh (the install-time network asks — IPv6 prefix + container subnet octet) are sourced, so they must use return, not exit.

Incus (not LXD)

The container runtime is Incus 7 LTS, installed from the Zabbly package repo (10-incus.sh) — never snap, never the lxc CLI.

  • Daemon socket: /var/lib/incus/unix.socket (group incus-admin). The panel daemon (vps.service) runs as the unprivileged vps user, which is a member of incus-admin; all Incus access is via the REST API over that socket.
  • internal/lx is the ONLY package that talks to Incus. Exec is done over the API websocket transport (Exec/ExecSH/RunInitScript) — there must be no incus/lxc CLI calls in the panel runtime path.
  • Storage pool vpsmgr (ZFS by default, bridge incusbr0). btrfs is supported as an alternative backend (on a btrfs host it becomes a native subvolume; elsewhere a loop file) — VPSMGR_STORAGE=btrfs. A dir backend exists only as an explicit VPSMGR_STORAGE=dir opt-in for test boxes — never a fallback; a failed pool (ZFS/btrfs) must abort, not downgrade. Container management is storage-driver-transparent in the Go panel; the only driver- aware code is lx.EnsureZFSRemoveSnapshots, which is a no-op off ZFS.
  • A remote-qualified fallback image (images:debian/13) must go through lx.EnsureImage before lx.Launch — the API cannot auto-fetch it inside the create call (unlike the old lxc launch CLI).

Privilege model

  • vps install runs as root (setup: user creation, units, kernel modules).
  • vps serve runs as the unprivileged vps user. Root commands are pinned in /etc/sudoers.d/vps (installed by ensureSudoers, validated with visudo -c); the panel escalates only those exact commands via internal/su (sudo -n). Never add a new exec.Command("...") that needs root without also adding it to the whitelist.

Documentation

  • docs/README.md — index
  • docs/architecture.md — system design (storage/network/security/bandwidth)
  • docs/configuration.md — /etc/vpsmgr/config.yaml reference
  • docs/ipv6.md — IPv6 pass-through design (large dev-branch feature)
  • docs/development.md — build/test/release/conventions

Keep the top-level READMEs concise; technical detail belongs in docs/ (English).

Key invariants — do not break

  • uninstall.sh without --purge must keep /etc/vpsmgr (config/db) and /etc/haproxy so reinstall adopts them; only --purge deletes them. A pre-HAProxy install's /etc/traefik is kept by the same rule (its config file is inert; the installer removes Traefik's binary/unit/account and the per-domain dynamic directory).
  • IPv6 bridge prefix is clamped to ≥ /64 (bridgePrefixLen): Incus's dnsmasq only serves /64 networks. Container addresses always live in the first /64 of the configured prefix.
  • IPv6 prefix length is required in ipv6_subnet — a bare address is rejected, never silently treated as /64.
  • Container hostnames are random (vps-<8hex>) and never equal the username.
  • install.sh --local-build must always rebuild (never reuse an installed binary) and warn about the branch. --update re-downloads the latest prebuilt release over an existing binary and must keep it untouched on download failure (no fallback rebuild).
  • Image builds (50-image.sh, 60-rhel-image.sh, 80-debian-dev-image.sh) must stay slim (apt/dnf/pacman clean) and delete the base image after publishing. 60-rhel-image.sh, 70-opensuse-image.sh, 80-debian-dev-image.sh and 90-arch-image.sh are optional and must never be part of install.sh (small boxes stay lean). 90-arch-image.sh is a rolling rebuild: it deletes the existing image and re-pulls upstream on every run.
  • Never add cgo or force C compilation.
  • The panel service is vps.service (restart via systemctl restart vps). Do not reintroduce vpsmgr-*.service names on the host. (The in-container vpsmgr-ipv6.service helper is a different thing — it lives inside container images.)

Conventions

  • One commit per small bug/feature; short subject + bullet body.
  • Run git log --oneline -10 for the current commit style before writing a message.
  • The test environment has ~2 GiB RAM; keep CI/local test workloads light.