# 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 | `~/.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 |