# 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 `<checkout>.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 <GOLDEN_VMID> <NEWID> --name <name> --full` → `qm set <NEWID> --ipconfig0 ip=192.168.13.<X>/24,gw=192.168.13.1` → `qm start <NEWID>` |
| 2 | OPNsense | NAT 80/tcp, 443/tcp, 443/udp from a free WAN alias → `192.168.13.<X>` |
| 3 | DNS | `A <panel-domain>` → that WAN alias (**before** step 4). App domains come later, per project. |
| 4 | VM | `ssh younes@192.168.13.<X>` → **`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://<panel-domain>/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/<THIS_VMID>/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 <old>`), 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
(`<br>`, `<b>`, …) is displayed literally, which is what broke the notes on
some older templates. Apply/refresh it with the `qm set --description` line
above (replace `<THIS_VMID>`); the same file can be pasted into the Notes
editor by hand.
