# Golden template: `paas-golden-r26` (VMID 9030 on prox3.younex.de) **One template, two flavours.** The template is always a generalized copy of the platform (OS, binaries, `paas` CLI + panel code, repo checkout) with NO instance state. Whether a clone becomes a **Node.js** server (= production today) or a **Laravel** server is decided at bootstrap time with `WITH_LARAVEL=1` — never by picking a different template. | VMID | Name | Status | |---|---|---| | 9030 | paas-golden-r26 | baked 2026-09-16 at commit `b4705e1`: r25 + **platform CI** — `bootstrap.sh` installs `paas-self-deploy` (root: tarball on stdin → lint → keep `.prev` → rsync → bootstrap with the instance's own settings → verify), the forced-command wrapper `paas-ci-ssh` and the exact-command sudoers `paas-ci`; all inert until `infra/setup-ci-deploy.sh` authorizes a CI key (instance state, never in the template). Production is updated by the LAN Jenkins job `paas-platform` on every push to `main` (`docs/ci-platform-deploy.md`). Also the panel branch selector is a preloaded dropdown again. Verified: Laravel-flavour clone via `paas-init --flavour laravel --yes` (panel online, `RUNTIMES=node,laravel`, `paas-self-deploy verify` ok, sudoers present, no CI key baked in). | **Policy: exactly ONE golden template exists.** Proxmox templates are immutable, so a refresh is always a new VMID; the previous template is destroyed as soon as the new one has produced a working clone (the r26 bake removed 9029, its immediate predecessor). The template is refreshed whenever new platform features land in production — see "Refreshing the template" below. Debian 13 with everything preinstalled: Node 22 (+18/20/24 under `/opt/node`), PM2, custom Caddy (`/usr/local/bin/caddy`, maxmind geo module) + systemd override, Varnish (loopback :6081, 2g), chromium, Checkmk agent (no TCP listener), `paas` CLI + panel code (with `node_modules`), all systemd timers, GeoIP mmdb, sudoers, deploy user (workstation key authorized), repo checkout at `/home/younes/dotnews-paas`. r20 adds the PHP 8.3/8.4 + composer + MariaDB packages with their services disabled (a Node clone never starts them). **Generalized** — contains NO instance state: no apps, no `paas.db`, no panel `.env`/secrets, no TLS certs/ACME account, no deploy SSH keypair, no database credentials/dumps, no logs/snapshots, empty machine-id, no SSH host keys (all regenerate on first boot via cloud-init / bootstrap). ## Spin up a new platform server — quick steps Every clone is a fully independent platform (own panel, own `paas.db`, own apps). The production panel is `deploy.younex.de`; **each new instance needs its own panel domain** (e.g. `deploy-php.younex.de`) — the wizard asks for it and it must resolve to the instance's WAN alias before Caddy can get a cert. | # | Where | Step | |---|---|---| | 1 | Proxmox | `ssh root@195.201.10.248` → `qm clone --name --full` → `qm set --ipconfig0 ip=192.168.13./24,gw=192.168.13.1` → `qm start ` | | 2 | OPNsense | NAT 80/tcp, 443/tcp, 443/udp from a free WAN alias → `192.168.13.` | | 3 | DNS | `A ` → that WAN alias (**before** step 4). App domains come later, per project. | | 4 | VM | `ssh younes@192.168.13.` → **`sudo paas-init`** — the wizard asks: panel domain, admin password, flavour **Node.js** (= production) or **Laravel** (+ PHP-FPM/composer/MariaDB), system-domain base, ACME e-mail; pulls the latest code; runs `bootstrap.sh`; prints what is left. Non-interactive: `sudo paas-init --panel-domain … --flavour laravel --password-file … --yes` | | 5 | Checkmk | host with the datasource-program rule pulling `https:///cmk-agent` with the `CMK_AGENT_TOKEN` the wizard printed (also in panel `.env`) | | 6 | Git hosts | add the deploy key the wizard printed (`/home/deploy/.ssh/id_ed25519.pub`, also in the panel) for private repos | What the flavour does: *Laravel* runs `bootstrap.sh` with `WITH_LARAVEL=1`, which configures PHP 8.3/8.4 (Sury), composer, MariaDB (loopback only), the `paas_admin` DB credentials, sudoers for fpm reloads, the scheduler + DB-backup timers, and writes `RUNTIMES=node,laravel` to `/srv/paas/paas.conf` — that line makes the panel show the *Laravel* runtime and the *Database* card. *Node.js* leaves all of that untouched (packages present but services disabled) and is identical to today's production server. A Laravel server can still host Node apps; the runtime is chosen **per project** at creation (`paas create --runtime laravel` / panel radio) and is immutable. `sudo paas-init` is safe to re-run (bootstrap is idempotent) — e.g. to turn a Node server into a Laravel-capable one later. ## Refreshing the template (next: r27) Everything below runs on a **throwaway clone** — production is never touched. ```bash # 1. Proxmox: clone the current template (or production, for fresher OS patches) ssh root@195.201.10.248 qm clone 9030 9031 --name paas-golden-r27 --full # current template -> next VMID qm set 9031 --ipconfig0 ip=192.168.13.250/24,gw=192.168.13.1 # any free lab IP qm start 9031 # 2. on the clone: bring the code to the commit the template should carry. # The VM has no GitHub credentials (by design — no secrets in the image), so # push the workstation checkout over the jump host instead of git pull. Do # NOT exclude .git here — the clone's checkout carries whatever .git the # previous template baked in, and skipping it leaves `git log` inside the # clone pointing at that stale commit even though the working tree is # current (harmless, but confusing — just sync .git too). DO exclude .claude # (Claude Code's own local session state, not part of the platform): rsync -az --delete --exclude .idea --exclude panel/node_modules --exclude .claude \ -e 'ssh -J root@195.201.10.248' ./ younes@192.168.13.250:dotnews-paas/ ssh -J root@195.201.10.248 younes@192.168.13.250 cd dotnews-paas && git log -1 --oneline # cloud-init runs an apt job on first boot — if bootstrap dies with # "Could not get lock /var/lib/dpkg/lock-frontend", wait for it: while sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do sleep 5; done # re-install CLI + panel from that checkout (plain Node bootstrap; the # generalize step wipes the panel .env / deploy key it generates) sudo PANEL_DOMAIN=golden.invalid PANEL_ADMIN_PASSWORD='x' ./bootstrap.sh # 3. generalize (pre-installs PHP/MariaDB packages, services left OFF) — powers off at the end. # Interactive safety gate: type GENERALIZE at the prompt (or `echo GENERALIZE |` it non-interactively). sudo PREINSTALL_LARAVEL=1 ./infra/generalize-golden.sh # 4. Proxmox: convert + set the notes (Markdown only — HTML shows as plain text) qm template 9031 qm set 9031 --description "$(sed 's//9031/' /path/to/dotnews-paas/infra/golden-template-notes.md)" ``` Then: clone the new template once as a Laravel server (`sudo paas-init --flavour laravel …`), confirm it sets up cleanly (panel reachable, `paas create --runtime laravel`, `paas db create`, a `laravel/laravel` deploy), a Node clone is implied (same bootstrap path as production). Update the table above, **destroy the previous template** (`qm destroy `), destroy the test clone. ### VM notes / summary `infra/golden-template-notes.md` is the canonical text for the Proxmox **Notes** panel of every golden template. It is pure Markdown (headings, lists, code spans, bold) because Proxmox renders Markdown only — any HTML tag (`
`, ``, …) is displayed literally, which is what broke the notes on some older templates. Apply/refresh it with the `qm set --description` line above (replace ``); the same file can be pasted into the Notes editor by hand.