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
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 |