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,76 @@
|
||||
# Application Design (consolidated) — atheos-blog-lxc
|
||||
|
||||
**Stage**: INCEPTION — Application Design (minimal depth) · **Date**: 2026-09-29
|
||||
**Deploy target**: Proxmox LXC `136` on node `scotty` (.69) — Alpine 3.23 · LAN-only
|
||||
|
||||
---
|
||||
|
||||
## 1. What this system is
|
||||
|
||||
A small, single-purpose guest whose only job is to put a **browser editor** in front of the
|
||||
`armistace/blog` working copy. It is deliberately **not** a build environment.
|
||||
|
||||
```
|
||||
┌───────────────────────── LXC 136 (scotty) ─────────────────────────┐
|
||||
Browser (LAN) │ │
|
||||
http://<ip>/ ────┼──► nginx :80 ──unix socket──► php-fpm (php83) ──► Atheos (PHP) │
|
||||
│ │ │
|
||||
│ ├──► workspace/blog/** (rw) │
|
||||
│ └──► git CLI ──► local commit│
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
(operator, later) SSH identity + SSH remote
|
||||
▼
|
||||
Gitea git.aridgwayweb.com
|
||||
```
|
||||
|
||||
## 2. Components
|
||||
|
||||
| ID | Component | Role |
|
||||
|---|---|---|
|
||||
| C1 | LXC `136` on scotty, Alpine 3.23 | isolated runtime, OpenRC, 20 GB root, onboot |
|
||||
| C2 | nginx :80 | HTTP front end; sends `*.php` to C3 |
|
||||
| C3 | php-fpm `php83` | PHP runtime; ZIP/OPENSSL/MBSTRING; `allow_url_fopen=On` |
|
||||
| C4 | Atheos (pinned SHA) | the editor; git via its `codegit` component |
|
||||
| C5 | `/var/www/atheos/workspace` | project storage (writable) |
|
||||
| C6 | git CLI | VCS; `safe.directory` set; **no identity** (operator-owned) |
|
||||
| C7 | `blog` working copy | clone of `armistace/blog` on `master` |
|
||||
| C8 | Gitea (external) | clone source; agent never pushes |
|
||||
| C9 | Operator (external) | owns SSH key, git identity, Atheos admin, remote switch |
|
||||
|
||||
## 3. Key interfaces
|
||||
|
||||
1. **nginx → php-fpm**: `fastcgi_pass unix:<FPM_SOCKET>` must equal the pool's `listen =`. Mismatch ⇒ **502**.
|
||||
2. **Atheos → filesystem**: writable `config.php`, `data/`, `workspace/`, `plugins/`, `themes/`. Failure ⇒ installer dependency error.
|
||||
3. **Atheos → git**: needs `git` on PATH **and** `safe.directory` for the workspace (else *dubious ownership*). Identity intentionally unset.
|
||||
4. **git → Gitea**: HTTPS clone (public); operator later switches to SSH.
|
||||
5. **OpenRC**: `nginx` + `php-fpm83` enabled at `default` runlevel — otherwise first reboot breaks service (AC-6).
|
||||
|
||||
## 4. Design decisions (from the gate)
|
||||
|
||||
| # | Decision | Source |
|
||||
|---|---|---|
|
||||
| D1 | **No git identity configured by the agent.** `user.name`/`user.email` left unset; RUNBOOK instructs the operator to set them first. | Q1=B |
|
||||
| D2 | **Atheos pinned to a resolved commit SHA**, with an additional **update script** that re-clones at latest and re-pins. | Q2 |
|
||||
| D3 | Web root `/var/www/atheos`; workspace `/var/www/atheos/workspace`; nginx :80; php-fpm via unix socket. | Q3 |
|
||||
| D4 | Atheos served at the **site root**. | assumed, accepted |
|
||||
| D5 | Operator completes Atheos's first-run installer; **agent seeds no users**. | FR-11 / Q2 of requirements |
|
||||
| **D6** | **Credential = a per-repository Gitea deploy key scoped to `armistace/blog`, write-enabled** (`read_only:false`), owned by the php-fpm user, aliased via `~/.ssh/config` with `IdentitiesOnly yes`. **Not** a PAT (account-wide scopes) and **not** a dedicated Gitea user (no blast-radius reduction for `blog`). Private key generated on the guest; public half handed to the operator to add in-repo. | Andrew, 2026-09-29 (security review) |
|
||||
|
||||
## 5. Scope guards (each is a testable negative)
|
||||
|
||||
- No database installed
|
||||
- **No Python / pelican / `make`** — the presence of Python is a defect (AC-8)
|
||||
- No NPM proxy host / DNS / port-forward (FR-10)
|
||||
- No `armistace` credential, SSH key, or git identity created by the agent (FR-11)
|
||||
|
||||
## 6. Design-level risks handed to Infrastructure Design
|
||||
|
||||
- **C-1** Atheos on Alpine is under-documented → resolve the FPM socket and extension package names from the *installed* system, not from upstream docs.
|
||||
- **C-2** OpenRC, not systemd → all service work uses `rc-service` / `rc-update`.
|
||||
- **C-3** Alpine template must be downloaded on scotty first (`pveam update` + `download`).
|
||||
- **C-4** The installer needs a writable web root before it will run.
|
||||
|
||||
## 7. Unit mapping
|
||||
|
||||
U1 guest (C1) · U2 web stack (C2,C3) · U3 Atheos (C4,C5) · U4 git + clone (C6,C7) · U5 docs + script (all)
|
||||
@@ -0,0 +1,49 @@
|
||||
# Component Dependencies — atheos-blog-lxc
|
||||
|
||||
**Stage**: INCEPTION — Application Design (minimal depth)
|
||||
|
||||
## Dependency matrix
|
||||
|
||||
Rows *depend on* columns. `→` = depends on, `—` = none.
|
||||
|
||||
| | C1 LXC | C2 nginx | C3 php-fpm | C4 Atheos | C5 workspace | C6 git | C7 blog |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| **C1 LXC** | — | — | — | — | — | — | — |
|
||||
| **C2 nginx** | ✔ | — | ✔ (I-1) | — | — | — | — |
|
||||
| **C3 php-fpm** | ✔ | — | — | — | ✔ (I-2) | — | — |
|
||||
| **C4 Atheos** | — | — | ✔ | — | ✔ | ✔ (I-3) | — |
|
||||
| **C5 workspace** | — | — | — | — | — | — | ✔ |
|
||||
| **C6 git** | ✔ | — | — | — | — | — | — |
|
||||
| **C7 blog** | — | — | — | — | ✔ | — | — |
|
||||
|
||||
**Reading it**: nothing works before C1; nginx is useless without php-fpm; Atheos depends on three
|
||||
things simultaneously; the clone is a leaf.
|
||||
|
||||
## Build / provisioning order (topological)
|
||||
|
||||
```
|
||||
C1 LXC
|
||||
├─► C2 nginx ──► (needs) C3 php-fpm ──► C4 Atheos ──► C5 workspace ──► C7 blog (clone)
|
||||
└─► C6 git ──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Critical path**: C1 → C2/C3 → C4 → C5 → C7
|
||||
- **Parallelisable**: nginx install and git install are independent
|
||||
|
||||
## External dependencies (version-pinned at build time)
|
||||
|
||||
| Dependency | Source | Pin strategy |
|
||||
|---|---|---|
|
||||
| Alpine rootfs | PVE template `alpine-3.23-default_20260116_amd64.tar.xz` | template filename is versioned |
|
||||
| `nginx`, `php83*`, `git` | Alpine `main`/`community` repos | record installed versions in the RUNBOOK at build time |
|
||||
| **Atheos** | `github.com/Atheos/Atheos` (master, no releases) | **pin to a resolved commit SHA** (design Q2); record it |
|
||||
| `armistace/blog` | Gitea, branch `master` | clone `master`; note the commit for the record |
|
||||
|
||||
## Coupling risks (where the design is tight)
|
||||
|
||||
| Coupling | Why it's tight | Mitigation |
|
||||
|---|---|---|
|
||||
| nginx ↔ php-fpm socket path | a string must match exactly; failure is a 502 with no other signal | resolve from the installed pool config, then assert both sides match |
|
||||
| FPM user ↔ workspace ownership | a UID mismatch yields "dubious ownership" or unwritable dirs | set ownership once from the actual FPM user, verify by writing a file |
|
||||
| template filename ↔ Alpine version | a wrong/renamed template fails `pct create` | confirm the exact filename with `pveam available` before creating |
|
||||
| Atheos master ↔ PHP 8.3 | upstream targets older PHP; 8.3 may warn | smoke-test the installer page and check the error log for PHP fatals |
|
||||
@@ -0,0 +1,65 @@
|
||||
# Component Methods / Interfaces — atheos-blog-lxc
|
||||
|
||||
**Stage**: INCEPTION — Application Design (minimal depth)
|
||||
**Note**: No business logic exists to specify. These are the **contracts between components** —
|
||||
the things that must match or the stack silently fails (the classic 502 / permission-denied class).
|
||||
|
||||
## I-1 · nginx → php-fpm (the interface most likely to break)
|
||||
|
||||
```
|
||||
nginx server block:
|
||||
listen 80;
|
||||
root /var/www/atheos;
|
||||
index index.php index.html;
|
||||
|
||||
location = /index.php { fastcgi_pass unix:<FPM_SOCKET>; ... }
|
||||
location ~ \.php$ { fastcgi_pass unix:<FPM_SOCKET>; ... }
|
||||
```
|
||||
|
||||
**Contract**: `<FPM_SOCKET>` in `nginx.conf` **must equal** the `listen =` path in the php-fpm pool.
|
||||
Alpine's `php83-fpm` default differs between packages — resolve it from the installed
|
||||
`/etc/php83/php-fpm.d/www.conf` rather than assuming.
|
||||
|
||||
- **Input**: HTTP request; **Output**: `text/html` from the PHP process
|
||||
- **Failure mode if mismatched**: `502 Bad Gateway` (socket missing) — AC-2 fails
|
||||
|
||||
## I-2 · Atheos → filesystem (the write contract)
|
||||
|
||||
Atheos's installer requires **write access** to exactly these paths, relative to `/var/www/atheos`:
|
||||
|
||||
| Path | Why | Mode |
|
||||
|---|---|---|
|
||||
| `config.php` | installer writes the config; file must exist | writable by FPM user |
|
||||
| `data/` | user accounts, settings | writable |
|
||||
| `workspace/` | projects (incl. the `blog` clone) | writable |
|
||||
| `plugins/` | plugin install/marketplace | writable |
|
||||
| `themes/` | theme install | writable |
|
||||
|
||||
- **Owner/group**: the php-fpm run user (resolved from the pool config)
|
||||
- **Failure mode**: installer shows a dependency error; AC-3 fails
|
||||
|
||||
## I-3 · Atheos (`codegit`) → git CLI
|
||||
|
||||
- **Requires**: `git` on `PATH`
|
||||
- **Requires**: `safe.directory` for the workspace — otherwise git refuses with
|
||||
`detected dubious ownership` because the FPM user differs from the filesystem owner
|
||||
- **Identity**: `user.name` / `user.email` are **deliberately UNSET** (design Q1=B).
|
||||
The operator sets them first thing after SSH (see RUNBOOK).
|
||||
- **Failure mode**: "dubious ownership" errors, or commit refused for missing identity
|
||||
|
||||
## I-4 · git CLI → Gitea
|
||||
|
||||
- **Remote (initial)**: `https://git.aridgwayweb.com/armistace/blog.git` (public ⇒ no credential)
|
||||
- **Remote (operator, later)**: `git@git.aridgwayweb.com:armistace/blog.git` — the RUNBOOK gives the
|
||||
exact `git remote set-url` command for after the key is added
|
||||
- **Contract**: the agent must **not** alter the remote to SSH itself (FR-6/FR-11)
|
||||
|
||||
## I-5 · OpenRC service contract (not systemd)
|
||||
|
||||
| Service | Enable | Purpose |
|
||||
|---|---|---|
|
||||
| `nginx` | `rc-update add nginx default` | HTTP |
|
||||
| `php-fpm83` | `rc-update add php-fpm83 default` | FastCGI (verify exact rc name on 3.23) |
|
||||
|
||||
- **Failure mode if not enabled**: works until first reboot, then AC-6 fails. This is why NFR-2 is
|
||||
verified with an actual reboot rather than a service-status check.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Components — atheos-blog-lxc
|
||||
|
||||
**Stage**: INCEPTION — Application Design (minimal depth)
|
||||
**Note**: This project has no application code to design. "Components" here are the **runtime
|
||||
pieces of the provisioned guest and their interfaces**, recorded so unit boundaries are defensible
|
||||
and a later agent inherits the map.
|
||||
|
||||
## Provisioned by the agent
|
||||
|
||||
| ID | Component | Purpose | Responsibilities |
|
||||
|---|---|---|---|
|
||||
| **C1** | **LXC guest** (`136` on scotty, Alpine 3.23) | The isolated runtime | OpenRC init, network (`eth0`, DHCP/static), 20 GB rootfs, timezone |
|
||||
| **C2** | **nginx** | HTTP front end | Listen `:80`; serve static assets; dispatch `*.php` to C3 over a unix socket |
|
||||
| **C3** | **php-fpm (`php83`)** | PHP FastCGI runtime | Execute Atheos PHP as a dedicated unprivileged user; extensions ZIP/OPENSSL/MBSTRING; `allow_url_fopen=On` |
|
||||
| **C4** | **Atheos** (upstream PHP app, pinned SHA) | The browser editor | File tree, editor, and git operations via its `codegit` component |
|
||||
| **C5** | **Atheos workspace** (`/var/www/atheos/workspace`) | Project storage | Holds working copies Atheos opens as projects; must be writable by C3 |
|
||||
| **C6** | **git CLI** | VCS client | Invoked by C4's `codegit`; needs `safe.directory`, has **no configured identity** (operator sets it) |
|
||||
| **C7** | **`blog` working copy** | The thing the operator edits | A clone of `armistace/blog` at `C5/blog`, on `master` |
|
||||
|
||||
## External / not provisioned by the agent
|
||||
|
||||
| ID | Component | Relationship |
|
||||
|---|---|---|
|
||||
| **C8** | **Gitea** (`git.aridgwayweb.com` → LXC 109, .155) | Clone source. **Read-only** from this project's perspective — the agent never pushes |
|
||||
| **C9** | **Operator (human)** | SSHes in; **owns**: SSH key, git identity, Atheos admin account, and switching the remote to SSH |
|
||||
|
||||
## Explicit non-components (scope guards)
|
||||
|
||||
- **No database.** Atheos is file-backed; nothing is installed for it.
|
||||
- **No Python / pelican / `make html`.** The build pipeline is out of scope (FR-1 / AC-8). The
|
||||
presence of Python would be a **defect**, not a bonus.
|
||||
- **No NPM proxy host, no DNS, no port-forward** (LAN-only, FR-10).
|
||||
- **No Atheos user seeded by the agent** — C9 creates the first account via the installer.
|
||||
@@ -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 |
|
||||
@@ -0,0 +1,61 @@
|
||||
# Requirement → Unit Map — atheos-blog-lxc
|
||||
|
||||
**Stage**: INCEPTION — Units Generation (Part 2)
|
||||
|
||||
> **Deliberate substitution.** The AI-DLC rules mandate `unit-of-work-story-map.md`, which maps
|
||||
> *user stories* to units. The **User Stories stage was skipped** (user-confirmed at the Requirements
|
||||
> gate: "no I'm good to skip"). With no stories to map, this document maps **requirements and
|
||||
> acceptance criteria** to units instead — the same assurance (nothing is silently dropped) applied
|
||||
> to the artifacts that actually exist.
|
||||
|
||||
## Functional requirements
|
||||
|
||||
| FR | Requirement (short) | Unit |
|
||||
|---|---|---|
|
||||
| FR-1 | Alpine 3.23 LXC on scotty, 20 GB root, 2 vCPU/1 GB, onboot, unprivileged | **U1** |
|
||||
| FR-2 | nginx + PHP-FPM serving Atheos; ZIP/OPENSSL/MBSTRING; `allow_url_fopen=On` | **U2** |
|
||||
| FR-3 | Atheos installed, pinned, 5 writable paths, no DB | **U3** |
|
||||
| FR-4 | git integration configured (git installed, `safe.directory`, identity handling) | **U4** |
|
||||
| FR-5 | `armistace/blog` cloned into `workspace/` on `master` | **U4** |
|
||||
| FR-6 | Remote reachable; instructions to switch to SSH; agent doesn't switch it | **U4** (build) + **U5** (document) |
|
||||
| FR-7 | RUNBOOK.md with IP, SSH, workspace path, remote-switch steps, credential setup | **U5** |
|
||||
| FR-8 | README.md | **U5** |
|
||||
| FR-9 | Repeatable provisioning script in the repo | **U5** |
|
||||
| FR-10 | LAN-only; no NPM/DNS/forward | **U2** (bind + no proxy config) |
|
||||
| FR-11 | Agent creates no `armistace` credentials or git identity | **U4** (verification asserts identity is empty) |
|
||||
| FR-12 | `docs/security-notes.md` + `docs/resiliency-notes.md` | **U5** |
|
||||
|
||||
## Non-functional requirements
|
||||
|
||||
| NFR | Requirement (short) | Unit |
|
||||
|---|---|---|
|
||||
| NFR-1 | Fits comfortably in 20 GB | **U1** (sizing) |
|
||||
| NFR-2 | Survives reboot with clone + edits intact | **U1** (onboot) + **U2** (services enabled) + **U3/U4** (persistence) → verified at Build & Test |
|
||||
| NFR-3 | Repeatable / idempotent-friendly provisioning | **U5** |
|
||||
| NFR-4 | No K3s dependency | all (negative) |
|
||||
| NFR-5 | Timezone Australia/Brisbane | **U1** |
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
| AC | Short | Unit |
|
||||
|---|---|---|
|
||||
| AC-1 | `pct config 136` shows Alpine + 20 GB root + onboot | **U1** |
|
||||
| AC-2 | `http://<ip>/` serves Atheos (not 502/404) | **U2**+**U3** |
|
||||
| AC-3 | `blog` present as a project whose files open | **U3**+**U4** |
|
||||
| AC-4 | Edit + save persists to disk | **U3**+**U4** |
|
||||
| AC-5 | `git remote -v` points at Gitea; RUNBOOK gives the SSH switch | **U4**+**U5** |
|
||||
| AC-6 | Reboot → Atheos serves again, clone + edits intact | Build & Test |
|
||||
| AC-7 | RUNBOOK lets another agent re-provision/hand off | **U5** |
|
||||
| AC-8 | **No Python/pelican present** (scope respected) | Build & Test |
|
||||
|
||||
## Coverage check
|
||||
|
||||
| | Count | Assigned to exactly one unit | Unassigned |
|
||||
|---|---|---|---|
|
||||
| Functional requirements | 12 | 12 ✔ | 0 |
|
||||
| Non-functional requirements | 5 | 5 ✔ | 0 |
|
||||
| Acceptance criteria | 8 | 8 ✔ | 0 |
|
||||
|
||||
**Every FR and NFR is assigned to exactly one unit.** ACs that require two units to build but one
|
||||
moment to verify (AC-2, AC-3, AC-4) list both; AC-6 and AC-8 are whole-system properties verified
|
||||
once at Build & Test rather than owned by a single unit — noted explicitly rather than left implicit.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Services / Request Flow — atheos-blog-lxc
|
||||
|
||||
**Stage**: INCEPTION — Application Design (minimal depth)
|
||||
|
||||
There is no service layer to design (no application code). What matters is the **two flows** the
|
||||
guest supports, and where each can break.
|
||||
|
||||
## Flow 1 — Operator edits and commits
|
||||
|
||||
```
|
||||
Browser (LAN)
|
||||
│ http://<lxc-ip>/
|
||||
▼
|
||||
nginx (C2, :80)
|
||||
│ location ~ \.php$ → fastcgi_pass unix:<FPM_SOCKET> [I-1]
|
||||
▼
|
||||
php-fpm (C3, php83)
|
||||
│ executes as the FPM user
|
||||
▼
|
||||
Atheos (C4, /var/www/atheos)
|
||||
├── reads/writes workspace/blog/** [I-2 write contract]
|
||||
└── on Commit: git (C6)
|
||||
│ safe.directory must include workspace/ [I-3]
|
||||
│ identity UNSET by design (Q1=B)
|
||||
▼
|
||||
commit created in the local clone
|
||||
```
|
||||
|
||||
**Break points**: FPM socket mismatch (502) · unwritable `workspace/` (installer/editor errors) ·
|
||||
dubious-ownership (git refuses) · missing identity (commit refused until operator sets it).
|
||||
|
||||
## Flow 2 — Operator pushes (operator-owned path)
|
||||
|
||||
```
|
||||
Atheos Commit → operator adds SSH key + sets git identity → remote switched to SSH (operator)
|
||||
│
|
||||
▼
|
||||
Gitea (C8) receives the push
|
||||
```
|
||||
|
||||
The **agent builds the path but does not walk it**: it clones over HTTPS, leaves the remote as-is,
|
||||
and documents the switch. Pushing is the operator's first hands-on act.
|
||||
|
||||
## Flow 3 — Build/publish (OUT OF SCOPE — happens elsewhere)
|
||||
|
||||
```
|
||||
Gitea (C8) → existing pipeline (NOT on this guest) → make html → published site
|
||||
```
|
||||
|
||||
Explicitly excluded (Q1=A). No Python, no pelican, no `make` on this guest. If this flow were
|
||||
expected here, **C-1's risk profile would change materially** — it is not.
|
||||
|
||||
## Provisioning order (what must exist before what)
|
||||
|
||||
```
|
||||
U1 guest → U2 nginx+php-fpm → U3 Atheos → U4 git + clone → U5 docs + script
|
||||
▲
|
||||
U4 also depends on U1 (git) ─────────────────┘
|
||||
```
|
||||
|
||||
Each arrow is a real dependency: installing Atheos before php-fpm leaves nothing to serve it;
|
||||
cloning before Atheos exists has no workspace to clone into.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Unit of Work Dependency — atheos-blog-lxc
|
||||
|
||||
**Stage**: INCEPTION — Units Generation (Part 2)
|
||||
|
||||
## Dependency matrix
|
||||
|
||||
Rows *depend on* columns.
|
||||
|
||||
| | U1 Guest | U2 Web stack | U3 Atheos | U4 Git+clone | U5 Docs |
|
||||
|---|---|---|---|---|---|
|
||||
| **U1 Guest** | — | — | — | — | — |
|
||||
| **U2 Web stack** | ✔ | — | — | — | — |
|
||||
| **U3 Atheos** | ✔ | ✔ | — | — | — |
|
||||
| **U4 Git+clone** | ✔ (git) | — | ✔ (workspace) | — | — |
|
||||
| **U5 Docs** | ✔ | ✔ | ✔ | ✔ | — |
|
||||
|
||||
## Build order
|
||||
|
||||
```
|
||||
U1 ──► U2 ──► U3 ──► U4 ──► U5
|
||||
│ ▲ │
|
||||
└─────────────────┴────────┘
|
||||
(U4 needs git from U1)
|
||||
```
|
||||
|
||||
**Critical path**: U1 → U2 → U3 → U4 → U5. Strictly sequential — no parallelism worth the
|
||||
coordination cost at this size. Each unit already includes a verification step, so a failure is
|
||||
caught at unit granularity rather than at the end.
|
||||
|
||||
## Why these boundaries
|
||||
|
||||
| Boundary | Reason |
|
||||
|---|---|
|
||||
| U1 before everything | nothing can be installed without the guest |
|
||||
| U2 (nginx+php) is one unit, not two | the **socket contract (I-1)** spans both — splitting them would create a boundary through a single interface, which is exactly where integration bugs live |
|
||||
| U3 after U2 | Atheos has nothing to run on until PHP exists |
|
||||
| U4 separate from U3 | **different failure class**: U3 is PHP-runtime risk (C-1); U4 is VCS/permissions risk. Separating them keeps a U3 failure from disguising itself as "git is broken" |
|
||||
| U4 before U5 | the RUNBOOK must document the real remote command, which only exists after the clone |
|
||||
| U5 last | docs describe what was actually built |
|
||||
|
||||
## Cross-unit risks
|
||||
|
||||
| Risk | Units | Note |
|
||||
|---|---|---|
|
||||
| FPM user identity | U2 → U3 → U4 | U2 resolves the run user; U3 sets ownership; U4 owns the key. **All three must agree on the same user** — a mismatch yields "dubious ownership" (I-3) |
|
||||
| FPM socket path | U2 (internal) | must match across nginx + pool |
|
||||
| Pinned Atheos SHA | U3 → U5 | U5's update script must re-pin the same mechanism |
|
||||
| Remote URL | U4 → U5 | U5 documents it; U4 must not change it to SSH |
|
||||
@@ -0,0 +1,89 @@
|
||||
# Unit of Work — atheos-blog-lxc
|
||||
|
||||
**Stage**: INCEPTION — Units Generation (Part 2: Generation)
|
||||
**Date**: 2026-09-29
|
||||
|
||||
A unit = a logical grouping of work that ends in a **verifiable state**.
|
||||
|
||||
---
|
||||
|
||||
## U1 — Guest provisioning
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Goal** | An Alpine 3.23 LXC exists, reachable, correctly sized, and boot-persistent |
|
||||
| **Components** | C1 |
|
||||
| **Produces** | VMID **136** on node `scotty`, 20 GB root, 2 vCPU, 1 GB RAM, unprivileged, `onboot=1`, TZ `Australia/Brisbane`, package index updated |
|
||||
| **Preconditions** | Alpine 3.23 template downloaded on scotty (`pveam update` + `download`) |
|
||||
| **Verification** | `pct config 136` shows Alpine + 20 GB root; the guest answers `ping`/`ssh`; date is Brisbane |
|
||||
| **ACs** | AC-1 |
|
||||
| **Notes** | Template download is part of this unit — it does not exist on scotty yet (risk C-3) |
|
||||
|
||||
## U2 — Web stack
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Goal** | nginx serves PHP, and the two sides agree on the socket |
|
||||
| **Components** | C2, C3 |
|
||||
| **Produces** | `nginx`, `php83-fpm`, `php83-{session,zip,openssl,mbstring}` installed; pool + vhost configured; `allow_url_fopen=On`; **both services enabled at the OpenRC `default` runlevel** |
|
||||
| **Preconditions** | U1 |
|
||||
| **Verification** | A `phpinfo()`-style probe renders over HTTP (**not** 502/404); the nginx `fastcgi_pass` socket path **string-matches** the pool's `listen` (interface I-1); both services are in `rc-update show` |
|
||||
| **ACs** | AC-2 |
|
||||
| **Notes** | Resolve the actual FPM socket from the installed pool config, not from upstream docs (risk C-1). OpenRC idioms only — no systemd (risk C-2) |
|
||||
|
||||
## U3 — Atheos install
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Goal** | Atheos runs and its installer page loads |
|
||||
| **Components** | C4, C5 |
|
||||
| **Produces** | Atheos at `/var/www/atheos` pinned to a resolved commit SHA; `config.php` present; `data/`, `workspace/`, `plugins/`, `themes/` writable by the FPM user |
|
||||
| **Preconditions** | U2 |
|
||||
| **Verification** | `http://<ip>/` returns the Atheos installer (HTTP 200, no PHP fatal in the error log); a write test inside `workspace/` succeeds as the FPM user |
|
||||
| **ACs** | AC-2, AC-3 |
|
||||
| **Notes** | **The genuinely uncertain unit** (risk C-1). If Atheos fights Alpine's PHP layout, stop and report rather than papering over it |
|
||||
|
||||
## U4 — Git + clone + deploy key
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Goal** | The operator can open `blog`, edit it, and push from here once they add the key |
|
||||
| **Components** | C6, C7 |
|
||||
| **Produces** | `git` installed; `safe.directory` set for the workspace; clone of `armistace/blog` (branch `master`) at `workspace/blog`; **ed25519 deploy keypair generated on the guest** (private key owned by the FPM user, mode 600); `~/.ssh/config` host alias `gitea-blog` with `IdentitiesOnly yes`; `user.name`/`user.email` **deliberately left unset** |
|
||||
| **Preconditions** | U1 (git), U3 (workspace exists) |
|
||||
| **Verification** | `blog` appears as a project and its files open; `git -C workspace/blog remote -v` shows Gitea; the **public** key is emitted for the operator; `git config --get user.name` returns **empty** (proving D1) |
|
||||
| **ACs** | AC-3, AC-4, AC-5 |
|
||||
| **Notes** | Two phases by necessity: this unit leaves the remote on **HTTPS**; the SSH switch happens *after* the operator installs the deploy key (D6). The agent must not install it |
|
||||
|
||||
## U5 — Docs, scripts, handoff
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Goal** | A different agent or a rebuild needs no prior context |
|
||||
| **Components** | all |
|
||||
| **Produces** | `README.md`, `RUNBOOK.md` (incl. **"set your git identity first"** prompt, the deploy-key install steps, and the exact `git remote set-url` command), `docs/security-notes.md`, `docs/resiliency-notes.md`, `scripts/provision.sh`, `scripts/update-atheos.sh` |
|
||||
| **Preconditions** | U1–U4 |
|
||||
| **Verification** | README + RUNBOOK exist and are linked; `provision.sh` is the exact sequence that was actually run; `update-atheos.sh` re-clones Atheos at latest and **re-pins the SHA** (D2) |
|
||||
| **ACs** | AC-7 |
|
||||
| **Notes** | The RUNBOOK is the operator-facing artifact: it must open with their required first action |
|
||||
|
||||
---
|
||||
|
||||
## Deferred / not a unit
|
||||
|
||||
- **The SSH remote switch** — operator-owned (D6). Documented in RUNBOOK, not built.
|
||||
- **Atheos admin user creation** — operator completes the installer (D5).
|
||||
- **Pelican build/publish** — out of scope entirely (Q1=A, AC-8).
|
||||
|
||||
## Greenfield code-organization strategy
|
||||
|
||||
No application code is generated, so the repo holds **only** provisioning + docs:
|
||||
|
||||
```
|
||||
atheos-blog-lxc/
|
||||
├── README.md
|
||||
├── RUNBOOK.md
|
||||
├── docs/{security-notes.md,resiliency-notes.md}
|
||||
├── scripts/{provision.sh,update-atheos.sh}
|
||||
└── aidlc-docs/ ← process trail only; never code
|
||||
```
|
||||
Reference in New Issue
Block a user