Static site on a public host: the second public host, and the control that draws the layer boundary #56

Closed
opened 2026-08-28 06:06:10 +01:00 by vnprc-agent · 2 comments

Stand up a static site on a rented internet-facing host, as the second public host in the fleet and the experiment that decides the layer boundary in allod/archetypes#55.

Primary goals:

  • A real deliverable — a project website served over HTTPS from a declarative machine definition, not a throwaway.
  • The negative control — a public host that carries nothing across a recreation, so the diff against a stateful deployment measures the layer boundary instead of guessing it.
  • Prove the base in isolation — DNS, ACME, reverse proxy, firewall, fail2ban, and remote installation, with no backup client, no restore tool, and no state gate.
  • Report a diff, not an opinion — the deliverable includes an enumerated list of what a stateful deployment has that this does not.

Why this one, and why now

allod/archetypes#55 argues that internet-facing machines split into a public-host base and a stateful-service layer, and that the axis is what must be carried across a recreation. A static site carries nothing: it is a build artifact of a repository, so recreating it is a rebuild, exactly like a dev guest.

That makes it the cheap case that tests the claim. If it turns out to need something from the stateful list, the two-layer model is wrong and the split should be drawn elsewhere. If it needs only the base, the boundary is measured rather than asserted, and the builder can be written against two real machines instead of one.

Building a second stateful service first would confirm nothing, because the shared lifecycle is already visible in the first one.

What it needs

  • A public DNS name, ACME certificates, and a reverse proxy serving static files.
  • A public firewall posture and fail2ban, matching what an existing public deployment already establishes.
  • Remote installation onto a rented host, and administrative SSH.
  • Content built from a repository and deployed by rebuilding the machine, with no state on the host worth preserving.

What it must not need

No backup client, no restore tool, no state-directory marker gating service startup, no secrets that must survive a rebuild, and no version pin driven by data compatibility. Each of these appearing anyway is a finding, and belongs in the report back to allod/archetypes#55.

Scope

In scope: the machine definition, its DNS and TLS, and the enumerated diff against a stateful public deployment.

Open and blocking a first commit: which repository owns this. It is not a profile — profiles compose a machine's user environment, and a service machine definition is a different kind of thing. Candidates are a small repository per service consumed as a flake input, or a shared repository for stateless public hosts. That decision is part of the same question allod/archetypes#55 asks, so it should be made deliberately rather than by whichever directory is convenient first.

Not in scope: generalizing anything. This issue produces one machine and one diff; the builder is written afterwards, against both machines.

Stand up a static site on a rented internet-facing host, as the second public host in the fleet and the experiment that decides the layer boundary in allod/archetypes#55. Primary goals: - **A real deliverable** — a project website served over HTTPS from a declarative machine definition, not a throwaway. - **The negative control** — a public host that carries nothing across a recreation, so the diff against a stateful deployment measures the layer boundary instead of guessing it. - **Prove the base in isolation** — DNS, ACME, reverse proxy, firewall, fail2ban, and remote installation, with no backup client, no restore tool, and no state gate. - **Report a diff, not an opinion** — the deliverable includes an enumerated list of what a stateful deployment has that this does not. ### Why this one, and why now allod/archetypes#55 argues that internet-facing machines split into a public-host base and a stateful-service layer, and that the axis is what must be carried across a recreation. A static site carries nothing: it is a build artifact of a repository, so recreating it is a rebuild, exactly like a dev guest. That makes it the cheap case that tests the claim. If it turns out to need something from the stateful list, the two-layer model is wrong and the split should be drawn elsewhere. If it needs only the base, the boundary is measured rather than asserted, and the builder can be written against two real machines instead of one. Building a second stateful service first would confirm nothing, because the shared lifecycle is already visible in the first one. ### What it needs - A public DNS name, ACME certificates, and a reverse proxy serving static files. - A public firewall posture and fail2ban, matching what an existing public deployment already establishes. - Remote installation onto a rented host, and administrative SSH. - Content built from a repository and deployed by rebuilding the machine, with no state on the host worth preserving. ### What it must not need No backup client, no restore tool, no state-directory marker gating service startup, no secrets that must survive a rebuild, and no version pin driven by data compatibility. Each of these appearing anyway is a finding, and belongs in the report back to allod/archetypes#55. ### Scope In scope: the machine definition, its DNS and TLS, and the enumerated diff against a stateful public deployment. Open and blocking a first commit: which repository owns this. It is not a profile — profiles compose a machine's user environment, and a service machine definition is a different kind of thing. Candidates are a small repository per service consumed as a flake input, or a shared repository for stateless public hosts. That decision is part of the same question allod/archetypes#55 asks, so it should be made deliberately rather than by whichever directory is convenient first. Not in scope: generalizing anything. This issue produces one machine and one diff; the builder is written afterwards, against both machines.
Author

Result: the experiment ran, and the two-layer model held

Settled 2026-08-28. A stateless public host definition exists and builds. All five prohibitions held — no backup client, no restore tool, no state-directory gate, no secrets that must survive a rebuild, no version pin driven by data compatibility — and each absence is asserted by a check rather than merely observed.

The measurement is DIFF.md in that repository: 262 lines, four categories, path:line on both sides. Eight rows differ because one host carries irreplaceable state, six because Forgejo is Forgejo, and eleven were identical. The services.openssh blocks came out byte-identical despite being written independently, which is the strongest single piece of evidence that a shared base was real rather than a coincidence of style.

Two findings worth carrying forward. The stateful host routes its root authorized_keys through agenix alongside four secrets that genuinely must survive; copying that here would have added secret management to a machine with no secrets and falsely "proved" the stateless host needed the stateful layer. And "carries nothing" has a rate limit: Caddy's ACME account key is regenerable and unpinned by clients, but Let's Encrypt caps duplicate certificates, so the criterion is irreversibility rather than the presence of a state directory.

Two things this issue does not cover

The site serves placeholder content. The machine ships an index.html and style.css written for the experiment. The real vnprc/hashpool-website repository was never wired in — it lives on the forge and was not cloned locally, so the search for existing content found nothing and fell through to a stub. Replacing it touches one file, by design: the module takes a path and does not care how it was produced.

The repository home is still undecided, and it blocks a first push. The recommendation is a repository of its own: this is a definition for a whole separate machine, and tucking it inside an existing repository means that repository rebuilds whenever the website changes and its history entangles with something unrelated.

Superseded scope

This issue framed one machine as a control experiment. The experiment is finished. How static sites should actually be organised — one host profile, a builder per generator, an entry per site, and CI-driven deploys — is a separate design and belongs in its own issue rather than here.

### Result: the experiment ran, and the two-layer model held Settled 2026-08-28. A stateless public host definition exists and builds. All five prohibitions held — no backup client, no restore tool, no state-directory gate, no secrets that must survive a rebuild, no version pin driven by data compatibility — and each absence is asserted by a check rather than merely observed. The measurement is `DIFF.md` in that repository: 262 lines, four categories, `path:line` on both sides. Eight rows differ because one host carries irreplaceable state, six because Forgejo is Forgejo, and eleven were identical. The `services.openssh` blocks came out byte-identical despite being written independently, which is the strongest single piece of evidence that a shared base was real rather than a coincidence of style. Two findings worth carrying forward. The stateful host routes its root `authorized_keys` through agenix alongside four secrets that genuinely must survive; copying that here would have added secret management to a machine with no secrets and falsely "proved" the stateless host needed the stateful layer. And "carries nothing" has a rate limit: Caddy's ACME account key is regenerable and unpinned by clients, but Let's Encrypt caps duplicate certificates, so the criterion is irreversibility rather than the presence of a state directory. ### Two things this issue does not cover **The site serves placeholder content.** The machine ships an `index.html` and `style.css` written for the experiment. The real `vnprc/hashpool-website` repository was never wired in — it lives on the forge and was not cloned locally, so the search for existing content found nothing and fell through to a stub. Replacing it touches one file, by design: the module takes a path and does not care how it was produced. **The repository home is still undecided**, and it blocks a first push. The recommendation is a repository of its own: this is a definition for a whole separate machine, and tucking it inside an existing repository means that repository rebuilds whenever the website changes and its history entangles with something unrelated. ### Superseded scope This issue framed one machine as a control experiment. The experiment is finished. How static sites should actually be organised — one host profile, a builder per generator, an entry per site, and CI-driven deploys — is a separate design and belongs in its own issue rather than here.
Member

Completed. The control experiment reported its measured result on 2026-08-28: the stateless public host needed the public-host base and none of the stateful lifecycle layer. The reusable result is now on master: nixosModules.publicHost at 3ba8082, followed by the directory-backed static-site module, Zola builder, adversarial content checks, and first-boot VM in #58 (e6a3ee4). The remaining production organisation and CI deployment work stays tracked in #57; the unfinished stateful layer/builder question stays in #55.

Completed. The control experiment reported its measured result on 2026-08-28: the stateless public host needed the public-host base and none of the stateful lifecycle layer. The reusable result is now on master: `nixosModules.publicHost` at `3ba8082`, followed by the directory-backed static-site module, Zola builder, adversarial content checks, and first-boot VM in #58 (`e6a3ee4`). The remaining production organisation and CI deployment work stays tracked in #57; the unfinished stateful layer/builder question stays in #55.
Sign in to join this conversation.
No description provided.