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.
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user