Build and Push Container / build-and-push (push) Successful in 23s
**Phase 03 final verification — all green** - Verified (all tasks already in `complete/`): snippet/Dockerfile/nginx.conf wiring, `scripts/check-headers.sh`, guardrail teeth, restored config byte-identical. - Tests (full suite): `./build.sh` → OK; `npm test` → **46 passed, 0 failed**; `bash .agents/validate.sh` → **exit 0** (46/46 + `HEADER CHECK PASSED` + hygiene gates); no leftover podman images. - Completion criteria: - `nginx-security-headers.inc` with 4 `add_header … always;` lines — ✅ - `nginx.conf`: 0 inline security `add_header` (cache headers only), 3/3 locations `include` snippet — ✅ - `server_tokens off;` in server block — ✅ (live proof: `Server: nginx`, no version) - `./scripts/check-headers.sh` → `HEADER CHECK PASSED`, OK on `/`, `/index.html`, hashed asset — ✅ (also confirmed all 4 headers + correct Cache-Control on 404 fallback) - `./build.sh && npm test` fully green — ✅ - No behavior change (terminal/vim/achievements/menu suites) — ✅ - Negative check re-run: dropping one `include` → `FAIL … on /style.<hash>.css`, exit 1 — ✅ - Docs: README.md & AGENTS.md reviewed in full against actual state (commands, ports 8123/18099, header mechanism, CI tags vs `.gitea/workflows/build-push.yml`, Node ≥20 engine floor, innerHTML/neofetch exception at `terminal.js:487`, simulated curl/wget guards) — **verified consistent, no changes needed**. - Notable: check-headers.sh aborts at the first failing path via `set -e` before printing `HEADER CHECK FAILED` — README's wording matches this exactly (verified empirically). PLAN.md's "44 tests" count is stale (now 46) but PLAN.md is locked — left for the user. - No live-infra changes (no HSTS in container; edge/Caddy untouched). - Next pending phase: `04_huntington_role`.
143 lines
5.9 KiB
Markdown
143 lines
5.9 KiB
Markdown
# Developer Homepage
|
|
|
|
A static portfolio site (vanilla HTML/CSS/JS + a fake terminal easter egg)
|
|
packaged as an nginx container and deployed with Podman Quadlet.
|
|
|
|
## Build & Run with Podman
|
|
|
|
### Build the image
|
|
|
|
```bash
|
|
podman build -t homepage:latest .
|
|
```
|
|
|
|
### Run the container
|
|
|
|
The image runs `build.sh` itself and copies the hashed `dist/` output into the
|
|
nginx html root, so the built site is already inside the image:
|
|
|
|
```bash
|
|
podman run --rm -p 0.0.0.0:8080:8080 homepage:latest
|
|
```
|
|
|
|
To iterate on `src/` without rebuilding the image, build locally and bind-mount
|
|
the **built** output over the html root — mount `dist/`, never `src/`, because
|
|
`index.html` requests content-hashed filenames:
|
|
|
|
```bash
|
|
./build.sh
|
|
podman run --rm -p 0.0.0.0:8080:8080 -v ./dist:/usr/share/nginx/html:z homepage:latest
|
|
```
|
|
|
|
Visit `http://localhost:8080` in your browser.
|
|
|
|
## Serving & configuration
|
|
|
|
`nginx.conf` is copied into the image as the site config. It listens on
|
|
**8080 over plain HTTP**, serves the hashed build, serves `index.html` with
|
|
`no-cache`, and caches the hashed assets as `immutable` for a year.
|
|
|
|
The four hardening headers (`X-Frame-Options`, `X-Content-Type-Options`,
|
|
`Referrer-Policy`, `Content-Security-Policy`) live in
|
|
`nginx-security-headers.inc`, which the Dockerfile copies to
|
|
`/etc/nginx/snippets/security-headers.inc`. nginx does **not** merge
|
|
`add_header` across configuration levels, so every `location` block `include`s
|
|
that snippet instead of inheriting from the `server` block — otherwise the
|
|
locations that set their own cache headers would silently drop the security
|
|
headers (audit finding M-4, Phase 03). `server_tokens off;` hides the nginx
|
|
version from the `Server` header (audit finding L-2). When adding a new
|
|
`location`, `include` the snippet there too and keep the security headers out of
|
|
`nginx.conf` itself.
|
|
|
|
TLS and HSTS are the job of the reverse proxy in front of the container
|
|
(`reeseapps.com` terminates at the Caddy edge). Do **not** add
|
|
`Strict-Transport-Security` or a `listen 443` block to `nginx.conf`.
|
|
|
|
### Verify the headers
|
|
|
|
```bash
|
|
./scripts/check-headers.sh
|
|
```
|
|
|
|
This is the integration test for the header fix: it builds the **real** image
|
|
with podman (or docker), runs it on the scratch address `127.0.0.1:18099`, and
|
|
asserts all four security headers on each response class — `/`, `/index.html`
|
|
and the first hashed asset found in `index.html`. On success it prints one
|
|
`OK:` line per path and `HEADER CHECK PASSED` (exit 0); a missing header prints
|
|
`FAIL: <header> missing on <path>` and exits non-zero, aborting at the first
|
|
failing path. No local `./build.sh` is needed — the image builds itself. The
|
|
container is removed by the script; `.agents/validate.sh` removes the leftover
|
|
`homepage-headercheck-*` image tag. Run it after **any** change to
|
|
`nginx.conf`, `nginx-security-headers.inc` or the `Dockerfile`.
|
|
|
|
## Deploy with Quadlet
|
|
|
|
The `homepage.container` Quadlet unit is **not** tracked in this repository — it
|
|
lives with the deployment host's config. Install it under
|
|
`~/.config/containers/systemd/` (copy it from wherever the host's unit files are
|
|
kept):
|
|
|
|
```bash
|
|
mkdir -p ~/.config/containers/systemd
|
|
cp <deploy-config>/homepage.container ~/.config/containers/systemd/
|
|
```
|
|
|
|
It must publish host port 8080 and use the image CI publishes:
|
|
`gitea.reeseapps.com/services/homepage` (`main` on every push to `main`, plus
|
|
one tag per release).
|
|
|
|
Quadlet is a systemd generator: it turns `homepage.container` into a
|
|
`homepage.service` unit when systemd regenerates units, so manage it under the
|
|
`.service` name:
|
|
|
|
```bash
|
|
systemctl --user daemon-reload
|
|
systemctl --user start homepage.service
|
|
systemctl --user status homepage.service
|
|
```
|
|
|
|
Auto-start on boot comes from the unit's `[Install]` section (e.g.
|
|
`WantedBy=default.target`), which the generator applies while generating — the
|
|
Quadlet services are transient, so `systemctl enable` has no effect on them;
|
|
re-run `systemctl --user daemon-reload` after editing the unit. For a user-mode
|
|
unit that must come up without a login session, enable lingering once:
|
|
`loginctl enable-linger $USER`.
|
|
|
|
## Development
|
|
|
|
Source files live in `src/`: `index.html`, `style.css`, `script.js` and the
|
|
flat terminal modules (`terminal.js`, `terminal-commands.js`,
|
|
`terminal-vim.js`, `terminal-achievements.js`). Keep every `.js` file flat in
|
|
`src/` — `build.sh` only hashes root-level files.
|
|
|
|
```bash
|
|
npm install # installs the pinned @playwright/test dev dependency
|
|
./build.sh # src/ -> dist/ (content-hashed asset names)
|
|
npm test # Playwright E2E suite against dist/ on 127.0.0.1:8123
|
|
```
|
|
|
|
The fake terminal performs **no real network I/O**. `curl` and `wget` render
|
|
deterministic simulated output built by `simulatedCurlOutput()` /
|
|
`simulatedWgetOutput()` in `src/terminal-commands.js`, which echo the requested
|
|
URL and say they are simulated (audit finding M-2, Phase 02) — add new command
|
|
output the same canned way. The E2E suite guards this: one test records every
|
|
request outside the test origin while running `curl` and `wget`, so a real
|
|
network call reintroduced into `src/` fails the suite. That test derives the
|
|
allowed origin from `baseURL` in `playwright.config.js`, so changing the port
|
|
there needs no test edit.
|
|
|
|
Always build before testing: the suite exercises the built artifact in `dist/`,
|
|
never `src/`. `npm test` starts (or reuses) its own static server, so no manual
|
|
server is needed. Rebuild the image (`podman build`) after changing `src/`.
|
|
|
|
Requires Node.js 20+ (the `@playwright/test` engine floor) and the Chromium
|
|
revision cached for the pinned Playwright version — run
|
|
`npx playwright install chromium` if it is missing, and see `AGENTS.md` before
|
|
bumping the pin.
|
|
|
|
The validation gate the phased pipeline runs per task is `.agents/validate.sh`
|
|
(full E2E suite, the container security-header check from
|
|
`scripts/check-headers.sh`, plus repo-hygiene checks): `bash
|
|
.agents/validate.sh`. The header check is skipped with a loud `WARN:` line on a
|
|
host with no container runtime; everything else still runs.
|