ops: phasefinal.com moved to Cloudflare Pages; pfi-ana-webhost destroyed and removed from inventory, DNS and the FortiGate
This commit is contained in:
@@ -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` |
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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 +0,0 @@
|
|||||||
lkraven@10.250.50.52
|
|
||||||
@@ -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/`
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
Executable
+19
@@ -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)"
|
||||||
Reference in New Issue
Block a user