Files
atheos-blog-lxc/aidlc-docs/design-artifacts/infrastructure-design.md
hermes a6fb61d0ea feat: provision Alpine LXC 136 running Atheos, wired to Gitea
Provisions a LAN-only browser code editor (Atheos) in an Alpine 3.23 LXC on the
scotty PVE node, with armistace/blog cloned and ready to edit. Built via the
AI-DLC workflow; the full process trail is in aidlc-docs/.

Deliverables
- scripts/provision.sh          rebuild the guest from scratch (re-runnable)
- scripts/update-atheos.sh      move Atheos to latest upstream and RE-PIN
- scripts/build-and-test.sh     captures evidence for every acceptance criterion
- files/atheos.conf             nginx vhost (socket path byte-matches the FPM pool)
- files/ssh-config              ~atheos/.ssh/config deploy-key alias
- RUNBOOK.md                    operational guide (START AT STEP 1)
- docs/security-notes.md        credential model + wider-web hardening TODOs
- docs/resiliency-notes.md      what a non-PoC deployment would need

Design decisions
- D1 no git identity is configured by the agent; the operator sets it. Git will
  refuse the first commit until then, which is deliberate (requirement FR-11).
- D2 Atheos pinned to commit f90cd18 (build 611) with a verified tarball sha256,
  plus update-atheos.sh as the deliberate escape hatch.
- D3 nginx :80, web root /var/www/atheos, php-fpm over a unix socket.
- D6 git auth is a per-repository DEPLOY KEY scoped to armistace/blog, not a
  personal access token (a PAT's write:repository scope reaches every repo the
  account can write).

Verified on the live guest (40 checks, 0 failures)
- AC-2  Atheos served over HTTP 200 by nginx+php-fpm; socket paths byte-match
- AC-3  20 posts + themes/cleanblog present; owned by the php-fpm user
- AC-4  all five writable paths writable as the service user
- AC-5  remote targets armistace/blog; deploy key authenticates over SSH
- AC-6  survival proven with a real container restart, marker byte-identical
- AC-8  no python/pelican/pip/make and no DB engine - scope boundary enforced
- FR-11 git identity is empty; deploy key mode 600 owned by the service user

Traps documented in the RUNBOOK (each cost real debugging time)
- The Gitea SSH user is `gitea`, NOT `git` (app.ini SSH_USER=gitea). With `git`,
  sshd answers "Invalid user git" and rejects before reading authorized_keys,
  which is indistinguishable from a bad key.
- The Gitea SSH endpoint is the LAN address (:22 is not forwarded publicly).
- `uptime` and `free` report the PVE HOST inside an LXC without lxcfs, so a
  container that just restarted can claim days of uptime.
2026-09-29 15:50:34 +10:00

7.3 KiB
Raw Permalink Blame History

Infrastructure Design — atheos-blog-lxc

Stage: CONSTRUCTION — Infrastructure Design Date: 2026-09-29 · Deploy target: Proxmox LXC (Alpine 3.23) on node scotty (.69)

No open questions. Every infrastructure decision was settled at the Requirements and Application Design gates (Q1–Q6, D1–D6). Re-asking them would be ceremony. This artifact maps the approved design onto concrete infrastructure, and records the provenance of each choice.


1. Guest specification (U1)

Property Value Provenance / rationale
Node scotty (192.168.178.69) user Q3
VMID 136 verified free — in-use on scotty: 104-107, 119, 125, 126, 133-135
OS Alpine 3.23 (alpine-3.23-default_20260116_amd64.tar.xz) user requirement ("alpine lxc")
Rootfs 20 GB user requirement (point 5)
Storage scotty-local-lvm (thin) 771 GB free at design time; thin keeps 20 GB nominal, minimal actual
vCPU 2 idle PHP app; 1 risks slow composer-less PHP ops
RAM 1024 MB nginx + php-fpm is trivial; scotty has 12.1 GB available
Swap 256 MB PHP can spike on large files; cheap insurance
Privileged? unprivileged default-safe; nothing here needs device access
onboot 1 NFR-2
Network eth0, bridge=vmbr0, DHCP matches all existing guests; LAN-only (FR-10)
DNS router-provided LAN-only
Timezone Australia/Brisbane NFR-5
Features none required ⚠️ see note below

⚠️ Note on LXC features

The observability project hit status=226/NAMESPACE because a container lacked nesting. That was systemd. This guest is OpenRC on Alpine, which does not need nesting. So: features left unset, and if a service fails to start we revisit nesting rather than pre-emptively enabling a feature that widens the container's escape surface.

Template provenance

The Alpine 3.23 template is not yet present on scotty (risk C-3). U1 therefore begins with pveam update + pveam download local alpine-3.23-default_..., and the download is verified before pct create runs.


2. Runtime stack (U2)

Layer Choice Notes
HTTP nginx (Alpine main) listens :80, root /var/www/atheos
PHP php83-fpm + php83 CLI Alpine 3.23's PHP is 8.3 — confirmed available (php83 present in the v3.23 index)
Extensions php83-session, php83-zip, php83-openssl, php83-mbstring Atheos: ZIP/OPENSSL/MBSTRING for the plugin marketplace; session is not optional
PHP ini allow_url_fopen=On Atheos marketplace requirement (recorded as accepted risk; turn off if unused — S-14)
Socket unix socket nothing extra listens on the LAN; path resolved from the installed pool config, not assumed

The socket contract (interface I-1 — the 502 risk)

nginx's fastcgi_pass and the php-fpm pool's listen are two strings that must be byte-identical. The design does not assume a path: U2 reads the pool config after install and writes nginx to match, then asserts both sides agree. A mismatch produces a bare 502 with no other diagnostic.


3. Application layer (U3)

Item Value
Web root /var/www/atheos
Source github.com/Atheos/Atheos, pinned to a resolved commit SHA (D2)
Database none — Atheos is file-backed
Writable paths config.php, data/, workspace/, plugins/, themes/
Ownership owned by the php-fpm run user (resolved from the pool)
Run user resolved at U2; U2/U3/U4 must agree on the same user (cross-unit risk)

4. VCS layer (U4)

Item Value Provenance
git Alpine package user point 2
safe.directory workspace path prevents dubious ownership (I-3)
Committer identity deliberately UNSET D1 / Q1=B — operator sets it first
Clone armistace/blog branch master → workspace/blog user point 3, Q6
Remote (initial) https://git.aridgwayweb.com/armistace/blog.git → HTTPS (public repo) U4 phase 1
Credential ed25519 deploy key, per-repo, write-enabled D6
Private key ~<fpmuser>/.ssh/id_ed25519_blog, mode 600, owned by FPM user D6
SSH alias Host gitea-blog + IdentitiesOnly yes D6 — a stray push cannot hit another repo
Remote (final) gitea-blog:armistace/blog.git U4 phase 2, after operator installs the key

5. Networking & exposure

  • LAN-only (FR-10). nginx binds :80 on the guest; nothing else is configured.
  • No NPM proxy host, no DNS record, no router forward — deliberately not created.
  • php-fpm on a unix socket ⇒ only :80 and :22 listen.
  • git.aridgwayweb.com resolves to a public IP — the clone traverses the internet, but the guest itself is not exposed. Noted because it makes Gitea-side scoping (D6) the meaningful control.

6. Operations & persistence (OpenRC, not systemd)

Service Enable command Failure if missed
nginx rc-update add nginx default serves until reboot, then AC-6 fails
php-fpm83 rc-update add php-fpm83 default same
  • Logs: /var/log/nginx/, php-fpm error log (path resolved at install)
  • Verification uses an actual reboot, not a service-status check — a service can be "started" and still not be enabled.
  • Monitoring/alerting: out of scope (R-3, S-12). Observability stack is a separate project.

7. Deployment architecture

                    LAN 192.168.178.0/24
                            │
                   ┌────────▼─────────┐
                   │  scotty (.69)    │  PVE node
                   │  ┌────────────┐  │
                   │  │ LXC 136    │  │  Alpine 3.23 · 2 vCPU · 1 GB · 20 GB root
                   │  │            │  │
      :80 ─────────┼──┤ nginx      │  │  unprivileged · onboot=1
                   │  │   │ unix   │  │
                   │  │   ▼ socket │  │
                   │  │ php-fpm83  │  │
                   │  │   │        │  │
                   │  │   ├─► /var/www/atheos{,/workspace/blog}
                   │  │   └─► git ─┼──┼──► gitea-blog:armistace/blog.git
                   │  └────────────┘  │        (deploy key, D6)
                   └──────────────────┘
                            │
                     internet (clone only)
                            ▼
                   git.aridgwayweb.com (Gitea LXC 109)

8. Infrastructure decisions summary

# Decision Why
I-1 Unprivileged, no features default-safe; nesting is a systemd need, not ours
I-2 Thin-provisioned scotty-local-lvm 20 GB nominal on 771 GB free; bounded blast radius
I-3 php-fpm over unix socket smallest network surface
I-4 Same user for FPM, file ownership, and key three-way identity agreement — the top cross-unit risk
I-5 No monitoring/alerting out of scope, recorded (R-3/S-12)
I-6 Reboot-based verification a "started" service is not an "enabled" one