ops: phasefinal.com moved to Cloudflare Pages; pfi-ana-webhost destroyed and removed from inventory, DNS and the FortiGate

This commit is contained in:
vh
2026-10-04 17:28:29 -07:00
parent 55e4d01b49
commit 513f6ef38f
19 changed files with 138 additions and 102 deletions
-1
View File
@@ -345,7 +345,6 @@ Observed and standardized across servers:
| fv-ml1 | 10.251.50.54 | Fountain Valley (`10.251.0.0/16`) | GPU / AI inference (bare metal, **4× RTX PRO 6000 Blackwell Max-Q, 96 GB each = 391 GB VRAM**) | `servers/fv-ml1/README.md` | | fv-ml1 | 10.251.50.54 | Fountain Valley (`10.251.0.0/16`) | GPU / AI inference (bare metal, **4× RTX PRO 6000 Blackwell Max-Q, 96 GB each = 391 GB VRAM**) | `servers/fv-ml1/README.md` |
| irv-ml1 | 100.64.0.6 (headscale mesh); 10.6.110.50 (Irvine LAN, routed via mesh) | Irvine — via headscale mesh; wg0 lifeline (10.100.79.3) dormant post-cutover | GPU / AI inference (bare metal, RTX 3090 + RTX A6000, native stacks) | `servers/irv-ml1/README.md` | | irv-ml1 | 100.64.0.6 (headscale mesh); 10.6.110.50 (Irvine LAN, routed via mesh) | Irvine — via headscale mesh; wg0 lifeline (10.100.79.3) dormant post-cutover | GPU / AI inference (bare metal, RTX 3090 + RTX A6000, native stacks) | `servers/irv-ml1/README.md` |
| ana-docker | 10.250.50.70 | Anaheim | General-purpose Docker host (non-GPU VM on pfi-pve) | `servers/ana-docker/README.md` | | ana-docker | 10.250.50.70 | Anaheim | General-purpose Docker host (non-GPU VM on pfi-pve) | `servers/ana-docker/README.md` |
| pfi-ana-webhost | 10.250.50.52 | Anaheim | VM on pfi-pve (VMID 110) — web workload | `servers/pfi-ana-webhost/README.md` |
| ana-filebot | 10.250.50.53 | Anaheim | LXC on pfi-pve (CT 112) — file-task automation | `servers/ana-filebot/README.md` | | ana-filebot | 10.250.50.53 | Anaheim | LXC on pfi-pve (CT 112) — file-task automation | `servers/ana-filebot/README.md` |
| pfi-pteradactyl | 10.250.50.55 | Anaheim | VM on pfi-pve (VMID 107) — Pterodactyl game panel | `servers/pfi-pteradactyl/README.md` | | pfi-pteradactyl | 10.250.50.55 | Anaheim | VM on pfi-pve (VMID 107) — Pterodactyl game panel | `servers/pfi-pteradactyl/README.md` |
| pfi-tacticalrmm | 10.250.50.57 | Anaheim | VM on pfi-pve (VMID 111) — TacticalRMM | `servers/pfi-tacticalrmm/README.md` | | pfi-tacticalrmm | 10.250.50.57 | Anaheim | VM on pfi-pve (VMID 111) — TacticalRMM | `servers/pfi-tacticalrmm/README.md` |
-1
View File
@@ -61,7 +61,6 @@ hosts:
- {name: ana-wg, site: ana, v4: 10.250.50.252, note: WireGuard host} - {name: ana-wg, site: ana, v4: 10.250.50.252, note: WireGuard host}
- {name: corviduo-dev, site: ana, v4: 10.250.50.152, note: Worldtree-team dev VM (PFI-hosted)} - {name: corviduo-dev, site: ana, v4: 10.250.50.152, note: Worldtree-team dev VM (PFI-hosted)}
- {name: pbs-ana, site: ana, v4: 10.250.50.90, note: Proxmox Backup Server — fleet primary} - {name: pbs-ana, site: ana, v4: 10.250.50.90, note: Proxmox Backup Server — fleet primary}
- {name: pfi-ana-webhost, site: ana, v4: 10.250.50.52, note: web workload}
- {name: pfi-postgres, site: ana, v4: 10.250.50.80, note: shared Postgres} - {name: pfi-postgres, site: ana, v4: 10.250.50.80, note: shared Postgres}
- {name: pfi-pteradactyl, site: ana, v4: 10.250.50.55, note: game panel} - {name: pfi-pteradactyl, site: ana, v4: 10.250.50.55, note: game panel}
- {name: pfi-tacticalrmm, site: ana, v4: 10.250.50.57, note: TacticalRMM} - {name: pfi-tacticalrmm, site: ana, v4: 10.250.50.57, note: TacticalRMM}
+1 -1
View File
@@ -34,7 +34,7 @@ every NIC. Linux = virtio, Windows = e1000. CTs take DHCP (reserved).
| 105 | PFI-Postgres | 10.250.50.80 | 16 (4×4) | 8 G | 80G ospool | Debian 11 | yes / order 3, up 60s | shared Postgres (vaultwarden/gitea/paperless); PGDATA local since 2026-04-23. `servers/pfi-postgres/` | | 105 | PFI-Postgres | 10.250.50.80 | 16 (4×4) | 8 G | 80G ospool | Debian 11 | yes / order 3, up 60s | shared Postgres (vaultwarden/gitea/paperless); PGDATA local since 2026-04-23. `servers/pfi-postgres/` |
| 106 | corviduo-dev | 10.250.50.152 | 8 (2×4, x86-64-v2-AES) | 8 G | 80G ospool | Debian 13 | yes / order 10 | Worldtree-team dev VM, PFI-hosted. `servers/corviduo-dev/` | | 106 | corviduo-dev | 10.250.50.152 | 8 (2×4, x86-64-v2-AES) | 8 G | 80G ospool | Debian 13 | yes / order 10 | Worldtree-team dev VM, PFI-hosted. `servers/corviduo-dev/` |
| 107 | PFI-Pteradactyl | 10.250.50.55 | 8 (2×4, host) | 8 G | 256G ospool | Debian 11 | yes / — | Pterodactyl game panel. `servers/pfi-pteradactyl/` | | 107 | PFI-Pteradactyl | 10.250.50.55 | 8 (2×4, host) | 8 G | 256G ospool | Debian 11 | yes / — | Pterodactyl game panel. `servers/pfi-pteradactyl/` |
| 110 | PFI-ANA-Webhost | 10.250.50.52 | 16 (4×4) | 4 G (balloon 1 G) | 250G ospool | Debian 11 | yes / order 30, up/down 120s | web workload. `servers/pfi-ana-webhost/` | | ~~110~~ | ~~PFI-ANA-Webhost~~ **destroyed 2026-10-04** (last image `pbs-ana:backup/vm/110/2026-10-04T10:28:18Z`) | ~~10.250.50.52~~ | 16 (4×4) | 4 G (balloon 1 G) | 250G ospool | Debian 11 | yes / order 30, up/down 120s | web workload. `servers/pfi-ana-webhost/` |
| 111 | pfi-tacticalrmm | 10.250.50.57 | 16 (4×4) | 8 G | 256G ospool | Linux | yes / order 20 | TacticalRMM. `servers/pfi-tacticalrmm/` | | 111 | pfi-tacticalrmm | 10.250.50.57 | 16 (4×4) | 8 G | 256G ospool | Linux | yes / order 20 | TacticalRMM. `servers/pfi-tacticalrmm/` |
All VMs have the QEMU guest agent enabled. All VMs have the QEMU guest agent enabled.
@@ -1,4 +1,6 @@
# pfi-ana-webhost — RETIRED 2026-10-04 # pfi-ana-webhost — DESTROYED 2026-10-04
> **Destroyed 2026-10-04 ~1726 (Prime: "retire the webhost — we may want one back in the future, and if so we'll build it fresh").** `qm destroy 110 --purge` on pfi-pve. Last image kept in PBS: `pbs-ana:backup/vm/110/2026-10-04T10:28:18Z` (+ the two before it). Cleaned up with it: FortiGate policy 18, address `ana-webhost-ip`, VIPs `web-to-webhost` + `ssh-to-webhost`, service `ssh-mapped-2223`, MAC object `pfi-ana-webhost`, DHCP reservation (server 5 entry 11); public A `pfi-ana-webhost.phasefinal.com`; internal `pfi-ana-webhost.ana.internal`. **38.120.12.45 is now unmapped.** FortiGate backups on nh3-dev: `/var/tmp/ana-gw-config-20261004T235515Z-pre-webhost-retire.conf`, `/var/tmp/ana-gw-config-20261005T002204Z-pre-webhost-objects-delete.conf`.
> **Retired 2026-10-04 (Prime: "retire dragonfirepro").** VM 110 shut down, `onboot 0`, out of the nightly vzdump > **Retired 2026-10-04 (Prime: "retire dragonfirepro").** VM 110 shut down, `onboot 0`, out of the nightly vzdump
> (last backup `pbs-ana:backup/vm/110/2026-10-04T10:28:18Z`; the VM is kept, not destroyed). FortiGate policy 18 > (last backup `pbs-ana:backup/vm/110/2026-10-04T10:28:18Z`; the VM is kept, not destroyed). FortiGate policy 18
+5 -5
View File
@@ -84,7 +84,7 @@ pfi-pve during morning backup window. Backup suspected as trigger
### pfi-pve (Proxmox VE, 10.250.250.31) ### pfi-pve (Proxmox VE, 10.250.250.31)
**Blast radius — everything hosted on it:** **Blast radius — everything hosted on it:**
- ana-docker (VM), ana-nas (CT 109), pfi-postgres (VM 105), pfi-ana-webhost, ana-filebot, pfi-pteradactyl, pfi-tacticalrmm, ana-wg, PBS-ANA (VM), + others - ana-docker (VM), ana-nas (CT 109), pfi-postgres (VM 105), ana-filebot, pfi-pteradactyl, pfi-tacticalrmm, ana-wg, PBS-ANA (VM), + others
Essentially **all Anaheim primary services** go offline. Because ana-nas lives here too, Tier-0 cascade applies simultaneously. Essentially **all Anaheim primary services** go offline. Because ana-nas lives here too, Tier-0 cascade applies simultaneously.
@@ -197,11 +197,11 @@ Essentially **all Anaheim primary services** go offline. Because ana-nas lives h
**Recovery:** `pct start 112`. **Recovery:** `pct start 112`.
### pfi-ana-webhost (VM on pfi-pve) — web workload ### ~~pfi-ana-webhost~~ — destroyed 2026-10-04
**Blast radius:** hosted website(s) offline. Retired and destroyed (Prime). Nothing in the fleet depends on it. Its sites live elsewhere: phasefinal.com on Cloudflare
Pages (`stacks/phasefinal-web/`), dragonfireacoustics.com on Cloudflare Pages in the client's account. Last image in PBS:
**Recovery:** VM restart; web server auto-start. `pbs-ana:backup/vm/110/2026-10-04T10:28:18Z`.
--- ---
+2 -1
View File
@@ -119,7 +119,8 @@ _As of 2026-10-04 ~0546 PT. The newest subsection is first; older subsections ca
### 2026-10-03/04: live now (as of 2026-10-04 ~0546 PT) ### 2026-10-03/04: live now (as of 2026-10-04 ~0546 PT)
- **pfi-ana-webhost RETIRED 2026-10-04 1657 (Prime: "retire dragonfirepro")**: VM 110 stopped, `onboot 0`, out of vzdump (backups kept); FortiGate policy 18 (WAN → HTTP/HTTPS/SSH to it) disabled, VIPs left inert; config backup on nh3-dev `/var/tmp/ana-gw-config-20261004T235515Z-pre-webhost-retire.conf`. ⭐ **dragonfireacoustics.com is NOT ours to serve:** Cloudflare Pages in the client's account since 2026-09-24 (source only on nh3-dev `~/development/adhoc/dragonfireacoustics-site`, no git remote; creds `nh3-dev/.config/cloudflare/dragonfireacoustics-com`); domain on Cloudflare Registrar to 2035-10-30, transfer-locked. **Supersedes the two 2026-09-17 dragonfire entries below** (expiry risk resolved; webhost no longer involved). → `servers/pfi-ana-webhost/README.md` - **pfi-ana-webhost DESTROYED 2026-10-04 ~1726 (Prime: retire, build fresh if ever needed)**: `qm destroy 110 --purge`; last image `pbs-ana:backup/vm/110/2026-10-04T10:28:18Z`. All FortiGate objects for it deleted (policy 18, VIPs, address, service, DHCP reservation; **38.120.12.45 now unmapped**), public + internal DNS records removed, inventory moved to `docs/retired/pfi-ana-webhost/`. ⭐ dragonfireacoustics.com = Cloudflare Pages in the CLIENT's account (source only on nh3-dev `~/development/adhoc/dragonfireacoustics-site`, no remote). **Supersedes the two 2026-09-17 dragonfire entries.**
- **phasefinal.com on Cloudflare Pages since 2026-10-04 ~1720** (project `phasefinal`, our account `vh@phasefinal`): www + apex CNAME → `phasefinal.pages.dev`; nginx container on ana-docker retired. Privacy page hosting line rewritten (Prime-approved wording). `_headers` + `404.html` added. ⚠ **Apex no longer 301s to www** (both serve) and the migration used **jackdaw's** Pages token: **Prime to mint `infra-ops-pages-token`** (Account Pages Edit + zone phasefinal.com DNS Edit + Single Redirect Edit), then add the apex→www Redirect Rule. Deploy: `stacks/phasefinal-web/deploy-pages.sh`.
- **VM 101 PFI-ANA-DC (pfi-pve) and VM 104 nh3-laser (nh3-pve) RETIRED from running (Prime 2026-10-04 1634: "we aren't running the DC or the remote laser")**: 101 shut down cleanly, both `onboot 0`, both excluded from their nightly vzdump (last backups kept). That also ends the PVE 9 Secure Boot/VirtIO backup warnings. ⚠ If either is ever started again: enroll the Microsoft 2023 certs first (`qm enroll-efi-keys`, BitLocker check). - **VM 101 PFI-ANA-DC (pfi-pve) and VM 104 nh3-laser (nh3-pve) RETIRED from running (Prime 2026-10-04 1634: "we aren't running the DC or the remote laser")**: 101 shut down cleanly, both `onboot 0`, both excluded from their nightly vzdump (last backups kept). That also ends the PVE 9 Secure Boot/VirtIO backup warnings. ⚠ If either is ever started again: enroll the Microsoft 2023 certs first (`qm enroll-efi-keys`, BitLocker check).
- **infra-ops login on pbs-ana + pbs-nh3** (Prime 2026-10-04): uid 850, fleet key, NOPASSWD + `log_output`, bootstrapped via guest agent; `servers/pbs-*/ssh-target` now infra-ops. - **infra-ops login on pbs-ana + pbs-nh3** (Prime 2026-10-04): uid 850, fleet key, NOPASSWD + `log_output`, bootstrapped via guest agent; `servers/pbs-*/ssh-target` now infra-ops.
- **PBS 3 → 4: DONE 2026-10-04 1144** (Prime via Miranda): pbs-nh3 then pbs-ana, both PBS 4.2.7 / Debian 13.7. VM backups first (pbs-ana had none anywhere), disk snapshots `pre-pbs4`, read-only datastores, detached dist-upgrades. Proven: v3→v4 sync, PVE 9→PBS 4 write (ct/113), v4→v4 sync. ⚠ My first prep killed apt mid-dpkg on both (qemu-guest-agent restarted itself under the in-band apt); repaired in ~5 min, PBS never stopped serving. **Open:** sfsrv-ana (PVE 8) 04:00 run on 10-05 is the one untested pair; drop `pre-pbs4` snapshots after it; vzdumps until ~10-11. → `docs/runbooks/pbs-pbs4-upgrade-plan.md` - **PBS 3 → 4: DONE 2026-10-04 1144** (Prime via Miranda): pbs-nh3 then pbs-ana, both PBS 4.2.7 / Debian 13.7. VM backups first (pbs-ana had none anywhere), disk snapshots `pre-pbs4`, read-only datastores, detached dist-upgrades. Proven: v3→v4 sync, PVE 9→PBS 4 write (ct/113), v4→v4 sync. ⚠ My first prep killed apt mid-dpkg on both (qemu-guest-agent restarted itself under the in-band apt); repaired in ~5 min, PBS never stopped serving. **Open:** sfsrv-ana (PVE 8) 04:00 run on 10-05 is the one untested pair; drop `pre-pbs4` snapshots after it; vzdumps until ~10-11. → `docs/runbooks/pbs-pbs4-upgrade-plan.md`
+1 -1
View File
@@ -26,7 +26,7 @@ PLAYBOOK=playbooks/bootstrap-infra-ops-user.yaml
# Tier 1 — compute / docker hosts (where the agent recurs) # Tier 1 — compute / docker hosts (where the agent recurs)
TIER1=(irv-ml1 ana-ml2 ana-docker nh3-docker nh3-dev) TIER1=(irv-ml1 ana-ml2 ana-docker nh3-docker nh3-dev)
# Tier 2 — PFI app VMs / LXCs # Tier 2 — PFI app VMs / LXCs
TIER2=(pfi-ana-webhost ana-filebot pfi-pteradactyl pfi-tacticalrmm ana-nas) TIER2=(ana-filebot pfi-pteradactyl pfi-tacticalrmm ana-nas)
# Tier 3 — sensitive infra (DB, backup, network, hypervisors): opt-in via PFI_TIER=all # Tier 3 — sensitive infra (DB, backup, network, hypervisors): opt-in via PFI_TIER=all
TIER3=(pfi-postgres pbs-ana pbs-nh3 ana-wg pfi-pve nh3-pve) TIER3=(pfi-postgres pbs-ana pbs-nh3 ana-wg pfi-pve nh3-pve)
+1 -1
View File
@@ -50,7 +50,7 @@ declare -A TARGET=(
[nh3-docker]=10.100.50.40 [nh3-extdev]=10.100.50.42 [nh3-docker]=10.100.50.40 [nh3-extdev]=10.100.50.42
[nh3-nas]=syncuser@10.100.50.50 [nh3-pve]=10.100.250.60 [nh3-nas]=syncuser@10.100.50.50 [nh3-pve]=10.100.250.60
[pbs-ana]=lkraven@10.250.50.90 [pbs-nh3]=lkraven@10.100.50.90 [pbs-ana]=lkraven@10.250.50.90 [pbs-nh3]=lkraven@10.100.50.90
[pfi-ana-webhost]=10.250.50.52 [pfi-gx10]=10.100.50.60 [pfi-gx10]=10.100.50.60
[pfi-postgres]=lkraven@10.250.50.80 [pfi-pteradactyl]=10.250.50.55 [pfi-postgres]=lkraven@10.250.50.80 [pfi-pteradactyl]=10.250.50.55
[pfi-pve]=10.250.250.31 [pfi-tacticalrmm]=10.250.50.57 [pfi-pve]=10.250.250.31 [pfi-tacticalrmm]=10.250.50.57
[vm-esh-nas]=lkraven@10.0.50.154 [vm-esh-nas]=lkraven@10.0.50.154
-1
View File
@@ -1 +0,0 @@
lkraven@10.250.50.52
+1 -1
View File
@@ -37,7 +37,7 @@ hypervisor):
- **VM 105** — `pfi-postgres` (PFI-Postgres; PGDATA on local VM disk as of 2026-04-23 migration off ana-nas NFS) — `servers/pfi-postgres/` - **VM 105** — `pfi-postgres` (PFI-Postgres; PGDATA on local VM disk as of 2026-04-23 migration off ana-nas NFS) — `servers/pfi-postgres/`
- **VM 106** — `corviduo-dev` (Worldtree-team dev VM, PFI-hosted) — `servers/corviduo-dev/` - **VM 106** — `corviduo-dev` (Worldtree-team dev VM, PFI-hosted) — `servers/corviduo-dev/`
- **VM 107** — `pfi-pteradactyl` (game panel) — `servers/pfi-pteradactyl/` - **VM 107** — `pfi-pteradactyl` (game panel) — `servers/pfi-pteradactyl/`
- **VM 110** — `pfi-ana-webhost` — **RETIRED 2026-10-04: stopped, `onboot 0`, out of vzdump** — `servers/pfi-ana-webhost/` - ~~**VM 110** — `pfi-ana-webhost`~~ — **destroyed 2026-10-04** (Prime); last image `pbs-ana:backup/vm/110/2026-10-04T10:28:18Z`; history in `docs/retired/pfi-ana-webhost/`
- **VM 111** — `pfi-tacticalrmm` — `servers/pfi-tacticalrmm/` - **VM 111** — `pfi-tacticalrmm` — `servers/pfi-tacticalrmm/`
- **CT 109** — `ana-nas` (Debian LXC serving NFS/SMB; bind-mounts - **CT 109** — `ana-nas` (Debian LXC serving NFS/SMB; bind-mounts
from ospool; data-serving SPOF — see `memory/incident_ana_nas_spof.md`) — `servers/ana-nas/` from ospool; data-serving SPOF — see `memory/incident_ana_nas_spof.md`) — `servers/ana-nas/`
+25 -25
View File
@@ -1,7 +1,11 @@
# phasefinal-web # phasefinal-web
The Phase Final, Inc. corporate site — nine static pages served by nginx on The Phase Final, Inc. corporate site — nine static pages on **Cloudflare Pages**
**ana-docker**, fronted by Traefik at `www.phasefinal.com`. (project `phasefinal`, account `vh@phasefinal`, `phasefinal.pages.dev`), at
`www.phasefinal.com` and `phasefinal.com`. Moved off ana-docker 2026-10-04 (Prime);
the nginx container there was retired (`/opt/docker/{compose,conf}/phasefinal-web.retired-20261004`).
`conf/site/` is the deployed tree, including `_headers` (the response headers nginx used
to set) and `404.html` (without it Pages serves the home page for every unknown path).
Built from the design brief at `docs/design/` (boothed 2026-08-29); the markup Built from the design brief at `docs/design/` (boothed 2026-08-29); the markup
and CSS came from a Claude Design session and are checked in verbatim under and CSS came from a Claude Design session and are checked in verbatim under
@@ -21,35 +25,31 @@ requests rule. Both must be undone on import:
Without these the page silently falls back to system fonts and looks wrong Without these the page silently falls back to system fonts and looks wrong
rather than broken, which is the failure mode you won't notice. rather than broken, which is the failure mode you won't notice.
## Routing — and the healthcheck trap that looks like a routing bug
Routers come from the compose labels: `phasefinal-web` serves
`www.phasefinal.com`; `phasefinal-apex` 301s `phasefinal.com` to it.
⚠ **Traefik silently skips containers Docker reports as unhealthy.** There is no
error, no log line, and no router — it looks exactly like a broken provider. The
first deploy of this stack hit that: the healthcheck used
`http://localhost/`, which resolves to `::1` in `nginx:alpine`, and nginx listens
on IPv4 only, so the check got "Connection refused" forever and the container
never left `unhealthy`. **Use `127.0.0.1`, never `localhost`, in a healthcheck
for an IPv4-only listener** — and if a correctly-labelled container never appears
in `/api/http/routers`, check `docker inspect --format '{{.State.Health.Status}}'`
*before* suspecting Traefik.
## Deploy ## Deploy
```bash ```bash
scripts/deploy-stack.sh ana-docker phasefinal-web stacks/phasefinal-web/deploy-pages.sh # conf/site → Pages production (branch main)
ssh infra-ops@10.250.50.70 'cd /opt/docker/compose/phasefinal-web && docker compose up -d'
``` ```
## DNS Needs a vaulted Cloudflare token with Account → Cloudflare Pages: Edit (see the script).
⚠ The 2026-10-04 migration used jackdaw's token as a one-off; a dedicated
`nh3-dev/.config/cloudflare/infra-ops-pages-token` is pending Prime. Check the result on
the per-deploy preview URL wrangler prints before trusting the custom domains.
`www.phasefinal.com` → A `38.120.12.44` (the Anaheim public IP, forwarded to ## DNS and redirects
ana-docker's Traefik). The apex `phasefinal.com` has **no** record — the site is
www-only by operator decision. Cloudflare-proxied is the intended end state, for `www.phasefinal.com` and `phasefinal.com` → CNAME `phasefinal.pages.dev`, Cloudflare-proxied
edge caching and the automatic email-address obfuscation the contact block (both are Pages custom domains). Cloudflare-proxied is also what provides the email-address
relies on. obfuscation the contact block relies on. ⚠ **The apex no longer 301s to www**: Traefik used
to do that, Pages `_redirects` cannot redirect between hostnames, and no vaulted token may
create a zone Redirect Rule. Both hostnames serve the site until Prime mints a token with
Single Redirect: Edit; then add the rule `phasefinal.com/* → https://www.phasefinal.com/${1}` (301).
## Lesson from the ana-docker era (2026-08-29)
Traefik silently skips containers Docker reports as unhealthy: no error, no router. The first
deploy's healthcheck used `http://localhost/`, which is `::1` in `nginx:alpine` while nginx
listened on IPv4 only. Use `127.0.0.1` in healthchecks for IPv4-only listeners.
## Content constraints ## Content constraints
+6 -4
View File
@@ -7,11 +7,13 @@ reviewable and reproducible rather than living only in someone's dashboard.
| name | type | value | proxied | | name | type | value | proxied |
|---|---|---|---| |---|---|---|---|
| `www.phasefinal.com` | A | `38.120.12.44` | yes | | `www.phasefinal.com` | CNAME | `phasefinal.pages.dev` | yes |
| `phasefinal.com` | A | `38.120.12.44` | yes | | `phasefinal.com` | CNAME | `phasefinal.pages.dev` | yes |
`38.120.12.44` is the Anaheim public IP, forwarded to ana-docker's Traefik. Since 2026-10-04 both are Cloudflare Pages custom domains (project `phasefinal`). They used to be
The apex 301s to `www` via a Traefik middleware, not a Cloudflare rule. A `38.120.12.44` → ana-docker's Traefik, which also 301'd the apex to www; that redirect is gone
until a Redirect Rule can be created (see `../README.md`). The cache rule below still applies and
is harmless with Pages as the origin (it respects origin headers).
## Cache ruleset ## Cache ruleset
-40
View File
@@ -1,40 +0,0 @@
services:
web:
image: nginx:alpine
container_name: phasefinal-web
restart: unless-stopped
volumes:
- /opt/docker/conf/phasefinal-web/site:/usr/share/nginx/html:ro
- /opt/docker/conf/phasefinal-web/nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- tnet
healthcheck:
# 127.0.0.1, NOT localhost: in this image `localhost` resolves to ::1 and
# nginx listens on IPv4 only, so `localhost` gives "Connection refused"
# forever. Traefik SKIPS unhealthy containers, so a wrong healthcheck here
# silently means "no route" rather than "unhealthy service".
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:80/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
labels:
- traefik.enable=true
- traefik.http.routers.phasefinal-web.rule=Host(`www.phasefinal.com`)
- traefik.http.routers.phasefinal-web.tls=true
- traefik.http.routers.phasefinal-web.tls.certresolver=anaprod
- traefik.http.services.phasefinal-web.loadbalancer.server.port=80
# apex -> www, 301
- traefik.http.routers.phasefinal-apex.rule=Host(`phasefinal.com`)
- traefik.http.routers.phasefinal-apex.tls=true
- traefik.http.routers.phasefinal-apex.tls.certresolver=anaprod
- traefik.http.routers.phasefinal-apex.service=phasefinal-web
- traefik.http.routers.phasefinal-apex.middlewares=phasefinal-to-www
- traefik.http.middlewares.phasefinal-to-www.redirectregex.regex=^https?://phasefinal\.com/(.*)
- traefik.http.middlewares.phasefinal-to-www.redirectregex.replacement=https://www.phasefinal.com/$${1}
- traefik.http.middlewares.phasefinal-to-www.redirectregex.permanent=true
networks:
tnet:
name: traefik-net
external: true
-18
View File
@@ -1,18 +0,0 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Static site; long cache on immutable assets, short on the document so
# a dictated edit goes live on the next deploy without a purge.
location = / { add_header Cache-Control "public, max-age=300"; try_files /index.html =404; }
location ~* \.(woff2|svg|png|ico)$ { add_header Cache-Control "public, max-age=31536000, immutable"; }
location ~* \.css$ { add_header Cache-Control "public, max-age=3600"; }
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy no-referrer always;
location / { try_files $uri $uri/ =404; }
}
+55
View File
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#0f1318">
<title>Page not found — Phase Final, Inc.</title>
<meta name="description" content="Page not found.">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/style.css?v=2026-09-28">
</head>
<body>
<a class="skip" href="#main">Skip to content</a>
<header class="site-header">
<div class="site-header-inner">
<a class="brand" href="/"><img src="/assets/pfi-wordmark-duotone-dark.svg" alt="Phase Final, Inc." width="98" height="28"></a>
<nav class="site-nav" aria-label="Site">
<ul>
<li><a href="/services/software/">Applications</a></li>
<li><a href="/services/systems-engineering/">Systems</a></li>
<li><a href="/services/hosting/">Hosting</a></li>
<li><a href="/services/ai/">AI</a></li>
<li><a href="/engagement/">Engagement</a></li>
<li><a href="/track-record/">Track record</a></li>
<li><a href="/contact/">Contact</a></li>
</ul>
</nav>
</div>
</header>
<header class="page-header">
<div class="hero-inner">
<div class="hero-copy">
<h1 class="lede">Page not found.</h1>
<p class="sublede">There is nothing at this address. <a href="/">Return to the home page</a>.</p>
</div>
</div>
</header>
<footer class="site-footer">
<span>Phase Final, Inc.</span>
<nav aria-label="Footer">
<a href="/services/software/">Applications</a>
<a href="/services/systems-engineering/">Systems</a>
<a href="/services/hosting/">Hosting</a>
<a href="/services/ai/">AI</a>
<a href="/engagement/">Engagement</a>
<a href="/track-record/">Track record</a>
<a href="/contact/">Contact</a>
<a href="/privacy/">Privacy</a>
</nav>
</footer>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
# Cloudflare Pages response headers (ported from the retired nginx.conf on ana-docker, 2026-10-04).
/*
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
! Access-Control-Allow-Origin
/fonts/*
Cache-Control: public, max-age=31536000, immutable
/assets/*
Cache-Control: public, max-age=31536000, immutable
/favicon.svg
Cache-Control: public, max-age=31536000, immutable
/style.css
Cache-Control: public, max-age=3600
@@ -46,7 +46,7 @@
<ul class="detail-list"> <ul class="detail-list">
<li><h3>What this site collects</h3><p>Nothing from your browser. Fonts and images are served from this site itself. No advertising or analytics scripts are loaded, so no cross-site profiling occurs on our side.</p></li> <li><h3>What this site collects</h3><p>Nothing from your browser. Fonts and images are served from this site itself. No advertising or analytics scripts are loaded, so no cross-site profiling occurs on our side.</p></li>
<li><h3>What email you send us</h3><p>Inquiries sent to inquiry@phasefinal.com are retained as ordinary business email for correspondence and engagement records. We do not add you to marketing lists; there are no marketing lists.</p></li> <li><h3>What email you send us</h3><p>Inquiries sent to inquiry@phasefinal.com are retained as ordinary business email for correspondence and engagement records. We do not add you to marketing lists; there are no marketing lists.</p></li>
<li><h3>Hosting</h3><p>This site is served from infrastructure the firm operates, fronted by Cloudflare for TLS and caching. Server logs exist for abuse control and are not joined to anything else.</p></li> <li><h3>Hosting</h3><p>This site is static files hosted on Cloudflare Pages, which serves it over TLS. The firm keeps no server logs for it; Cloudflare processes requests under its own privacy policy.</p></li>
<li><h3>Questions</h3><p>Write to <a href="mailto:inquiry@phasefinal.com">inquiry@phasefinal.com</a>.</p></li> <li><h3>Questions</h3><p>Write to <a href="mailto:inquiry@phasefinal.com">inquiry@phasefinal.com</a>.</p></li>
</ul> </ul>
<p class="related"><a href="/">Home →</a></p> <p class="related"><a href="/">Home →</a></p>
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Deploy phasefinal.com to Cloudflare Pages (project `phasefinal`, account vh@phasefinal).
# stacks/phasefinal-web/deploy-pages.sh
# The site is the static tree in conf/site/ (with its _headers and 404.html); there is no build step.
# Token: a Cloudflare API token with Account → Cloudflare Pages: Edit, read from the vault.
# PAGES_TOKEN_KEY defaults to the dedicated infra-ops token (to be minted by Prime);
# the 2026-10-04 migration used nh3-dev/development/jackdaw/cloudflare-api-token as a one-off.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
key=${PAGES_TOKEN_KEY:-nh3-dev/.config/cloudflare/infra-ops-pages-token}
CLOUDFLARE_API_TOKEN=$(secret get "$key") || { echo "no token at vault key $key (set PAGES_TOKEN_KEY)" >&2; exit 2; }
export CLOUDFLARE_API_TOKEN
CLOUDFLARE_ACCOUNT_ID=$(curl -s -m 15 -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" https://api.cloudflare.com/client/v4/accounts \
| python3 -c 'import json,sys; r=json.load(sys.stdin)["result"]; print(next(a["id"] for a in r if a["name"]=="vh@phasefinal"))')
export CLOUDFLARE_ACCOUNT_ID
# run wrangler from a scratch dir so it never drops .wrangler/ state into the repo
cd "$(mktemp -d)"
npx -y wrangler@4 pages deploy "$here/conf/site" --project-name phasefinal --branch main --commit-dirty=true \
--commit-message "deploy from eshpfi-management $(git -C "$here" rev-parse --short HEAD)"