10 KiB
10 KiB
Deployment log — CTAO portal demo on strapi-experimental.cyfronet
Living document: what was done, what broke, what is left. Update it as work proceeds so any agent (or human) can resume without the chat history. Started 2026-07-28.
Goal
Run the git-based CMS demo (Gitea + Sveltia + Astro static build) on the
Cyfronet test machine so the SUSS-PORT team can click through it. Shape must
be production-viable, not throwaway. Machine footprint: one directory
(~/ctao-portal-demo/) + 4 unit files, fully removable (README "Uninstall").
Ground rules (from the user, non-negotiable)
- Write every command out in chat, marked read-only / write, BEFORE running it — the user reviews on a phone and cannot see truncated tool calls.
- Non-invasive verification first, debugging second: confirm each precondition instead of fixing after the fact.
- Never read secrets (
~/.ssh/*, tokens). Mask token URLs in any output (sed 's|://[^@]*@|://***@|g'). Secrets stay in 0600 files on the machine. - No sudo, nothing outside
$HOMEon the machine, no traces for the admin. - Temp/working files under
.tmp/<date>-<topic>/in the repo, never/tmp. - Commit messages: no "Co-Authored-By: Claude", no session links.
Architecture (decided, see also README.md in this dir)
editor → Sveltia (/admin, static) → commit → Gitea :3000 (container, SQLite)
│ polled every 10 s
build.sh (systemd user timer)
└─ podman run node+git, --network=container:ctao-demo-gitea
npm ci (only if lockfile changed) → astro build
→ releases/<sha>/ → atomic symlink flip
│
nginx :8080 serves releases/current
│ (TODO) ingress vhost → team access
Machine facts (verified 2026-07-28, read-only)
- Rocky Linux 9.7, host
outline.openstacklocal, IP 192.168.10.15, 2 vCPU, 3.6 GB RAM, no swap. Userstrapi, no sudo,Linger=yes, user systemd running. - Podman 5.6.0 rootless. Host has node 24.17, npm, curl, jq, rsync — no git, no nginx (hence the build container / nginx container).
- Neighbours: pod-strapi (
:1337), Outline (:9091), code-server (127.0.0.1:41787). - VPN does NOT route to machine ports (
curl 192.168.10.15:1337times out); only the admin-managed HTTPS ingress reaches the machine (https://strapi.isl-dev.grid.cyfronet.pl→ 204). Hence ports 3000/8080 are reachable for us only viassh -L, and the team needs vhosts from Hubert.
DONE
1. Local: files authored + reviewed + tested (before touching the machine)
- Wrote quadlets,
build.sh, systemd service/timer,nginx.conf,Containerfile.build,README.md(runbook incl. uninstall). - Opus security review — 8 findings, every one verified personally:
BLOCKER
--network=host(build code could reach host loopback: code-server = RCE, Strapi) → replaced;OnUnitActiveSecis start-relative, not end-relative (agent corrected me) →OnUnitInactiveSec; missingmkdirfor~/.config/*; nginx-before-first-build race; prunepipefaillandmine; noisy unit failure when Gitea is down; CORSSCHEMEnote; RAM headroom. All fixed. No hallucinated findings. - Local end-to-end test against the local Gitea (
:3010), sandbox.tmp/2026-07-28-deploy-test/: cold build 23 s · no-op poll 45 ms · incremental rebuild 4 s · prune ✓ · Gitea-down grace ✓ · anonymous clone (no token in.git/config) ✓. - Commits (local Gitea repo
ctao/portal):ed7a9c0,80d886c.
2. Machine: install steps 1–7 of README
- Created
~/ctao-portal-demo/{gitea-data,gitea-config,releases,state,bin,config}and~/.config/{containers/systemd,systemd/user}. - Copied unit files,
build.sh,nginx.conf,Containerfile.build. - Pulled + pinned by digest: gitea 1.27-rootless, nginx stable-alpine,
node:24-alpine; built
localhost/ctao-portal-build:1. - Gitea running (
:3000, API version 1.27.1), adminctaocreated with--random-password→~/ctao-portal-demo/state/initial-admin.txt(0600), push token →state/push-token.txt(0600). Never printed in chat. - Content pushed via git bundle (78 MB, one-off bootstrap): repo
ctao/portalat80d886c, public, anonymously clonable. Bundle deleted afterwards. - First build on the machine: 44 s (
npm ci~35 s + astro build 6.5 s), 225 pages. Incremental rebuilds should be ~10 s (measure to confirm). - nginx container running,
http://localhost:8080/→ 200,<title>CTAO Science Portal</title>. - Poll timer enabled (
ctao-portal-build.timer), ticking every ~10 s, journal clean.
3. Problems hit on the machine (and the fixes)
- Rootless bridge network impossible:
netavark: modprobe ip_tables: Operation not permitted→ quadlet restart loop. Root cause: kernel module not loaded, rootless cannot modprobe, no sudo. Fix: droppedctao-demo.network; build joins Gitea's netns (--network=container:ctao-demo-gitea), so it reaches Gitea onlocalhost:3000while the host loopback stays unreachable (pasta). Same security property as the bridge, zero privileges. Lesson written to.skills/podman-quadlet/SKILL.md. - Push rejected: admin was created with
--must-change-password→remote: Update your password. Fix:gitea admin user must-change-password --unset ctao. - My rc-chain bug:
... | sedmade$?the sed's status, so a failed push printed "push-ok" and the bundle was deleted prematurely → had to re-upload 78 MB. Fix: capture output in a var, check rc of the real command. (Also why the upload "took so long" — it ran twice.) - Push-created repo was private despite
DEFAULT_PRIVATE=public— push-to-create has its own key. Fix: PATCH via API to public +GITEA__repository__DEFAULT_PUSH_CREATE_PRIVATE=falsein the quadlet. npm cifailed:ETXTBSYspawningesbuild(postinstall race in a rootless container, ~5 min wasted). Fix:npm ci --ignore-scripts(also removes the malicious-postinstall vector) + persistent npm cache volume~/ctao-portal-demo/npm-cache. Validated locally, then on machine.
4. Left on the machine (known, not cleaned — user asked to be told, not tidied)
- Orphan systemd entry
ctao-demo-network.service(not-found/active-exited), ghost of the removed network quadlet. Harmless; clears withsystemctl --user stop ctao-demo-network.service. /tmp/storage-run-1003/(~136 KB) — podman runroot from ssh calls withoutXDG_RUNTIME_DIR. Disappears on reboot. (storage-run-1001is NOT ours.)- Red journal entries from the netavark restart loop (history, not a live fault).
- Disk: 11 G → 9.6 G free (images ~590 MB + repo/content). RAM available ~1.3 G.
TODO (next agent starts here)
- Commit the last local fix (
build.sh:--ignore-scripts+ npm cache) to the local Gitea repo, then mirror it to the machine's Gitea so the machine's copy of the repo matches. NOTE:build.shcurrently lives in the same repo as the content; pushing to the machine repo re-triggers a build (that is fine and is a good live test of the pipeline). - Measure an incremental rebuild on the machine (push a trivial content change → time from commit to visible change). Expect ~10–20 s total. Record the number here; it is the "publish latency" figure for the demo.
- Verify the whole loop through
ssh -Lwith the user watching:ssh -L 8080:localhost:8080 -L 3000:localhost:3000 strapi-experimental.cyfronet→ portal athttp://localhost:8080, Gitea athttp://localhost:3000. Note: forwarding the SAME port numbers keeps Gitea'sROOT_URLvalid, so Sveltia's OAuth login can be tested end-to-end before any vhost exists. - Sveltia OAuth app: create in Gitea UI (Settings → Applications), PKCE,
redirect to the portal
/admin/; put URL + client id inpublic/admin/config.yml; commit → auto-publishes. Then test a real edit from the browser (the demo's money shot). - Message to Hubert (only after we have seen it working):
- two ingress vhosts →
192.168.10.15:8080(portal) and:3000(gitea), same mechanism asstrapi.isl-dev…:1337; - are vhosts public-internet or VPN-scopable? (Gitea preferably VPN-only);
- does Cyfronet offer static-file hosting on the ingress itself? If yes we drop our nginx container and just rsync builds there.
- two ingress vhosts →
- After vhosts arrive: replace both
TODO(vhost)inctao-demo-gitea.container(ROOT_URL, CORS origin +GITEA__cors__SCHEME=https),daemon-reload+ restart, update Sveltia config, re-test login. - Backup: nightly
tarof~/ctao-portal-demo/gitea-data(the only stateful thing; everything else is rebuildable). Not yet written. - Decided but not implemented — split into two repos (
portal= code,portal-content= markdown + uploads). Rationale: editors get write access ONLY to content, so repo-push cannot alterpackage.json/templates that the build executes; also keeps code history clean. Cost:build.shclones two repos and overlays content (~10 lines), polls two SHAs. Do this for the production version, not mid-demo. - Demo choreography: the test article is
draft: true; the user creates one live during the demo. With the poller running, a Sveltia save publishes automatically — no manual rebuild needed (unlike the Mac preview setup).
Command cheat-sheet (machine)
export XDG_RUNTIME_DIR=/run/user/$(id -u) # needed for systemctl --user over ssh
systemctl --user status ctao-demo-gitea ctao-demo-web ctao-portal-build.timer
journalctl --user -u ctao-portal-build -n 20 --no-pager # build times land here
~/ctao-portal-demo/bin/build.sh # manual build
readlink ~/ctao-portal-demo/releases/current # which sha is live
curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/