Files
portal/deploy/DEPLOY-LOG.md
T

10 KiB
Raw Blame History

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 $HOME on 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. User strapi, 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:1337 times 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 via ssh -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; OnUnitActiveSec is start-relative, not end-relative (agent corrected me) → OnUnitInactiveSec; missing mkdir for ~/.config/*; nginx-before-first-build race; prune pipefail landmine; noisy unit failure when Gitea is down; CORS SCHEME note; 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), admin ctao created 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/portal at 80d886c, 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)

  1. 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: dropped ctao-demo.network; build joins Gitea's netns (--network=container:ctao-demo-gitea), so it reaches Gitea on localhost:3000 while the host loopback stays unreachable (pasta). Same security property as the bridge, zero privileges. Lesson written to .skills/podman-quadlet/SKILL.md.
  2. Push rejected: admin was created with --must-change-password → remote: Update your password. Fix: gitea admin user must-change-password --unset ctao.
  3. My rc-chain bug: ... | sed made $? 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.)
  4. 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=false in the quadlet.
  5. npm ci failed: ETXTBSY spawning esbuild (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 with systemctl --user stop ctao-demo-network.service.
  • /tmp/storage-run-1003/ (~136 KB) — podman runroot from ssh calls without XDG_RUNTIME_DIR. Disappears on reboot. (storage-run-1001 is 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)

  1. 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.sh currently 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).
  2. 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.
  3. Verify the whole loop through ssh -L with the user watching: ssh -L 8080:localhost:8080 -L 3000:localhost:3000 strapi-experimental.cyfronet → portal at http://localhost:8080, Gitea at http://localhost:3000. Note: forwarding the SAME port numbers keeps Gitea's ROOT_URL valid, so Sveltia's OAuth login can be tested end-to-end before any vhost exists.
  4. Sveltia OAuth app: create in Gitea UI (Settings → Applications), PKCE, redirect to the portal /admin/; put URL + client id in public/admin/config.yml; commit → auto-publishes. Then test a real edit from the browser (the demo's money shot).
  5. Message to Hubert (only after we have seen it working):
    • two ingress vhosts → 192.168.10.15:8080 (portal) and :3000 (gitea), same mechanism as strapi.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.
  6. After vhosts arrive: replace both TODO(vhost) in ctao-demo-gitea.container (ROOT_URL, CORS origin + GITEA__cors__SCHEME=https), daemon-reload + restart, update Sveltia config, re-test login.
  7. Backup: nightly tar of ~/ctao-portal-demo/gitea-data (the only stateful thing; everything else is rebuildable). Not yet written.
  8. 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 alter package.json/templates that the build executes; also keeps code history clean. Cost: build.sh clones two repos and overlays content (~10 lines), polls two SHAs. Do this for the production version, not mid-demo.
  9. 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/