Files
portal/deploy
ctaoandClaude Fable 5 ed7a9c01e7 deploy: machine deployment — quadlets (gitea, nginx, isolated build network), poll+build pipeline, runbook
Reviewed (Opus security review, all findings verified) and tested end-to-end
locally: cold build 23s, no-op poll 45ms, incremental rebuild 4s, prune,
gitea-down grace, real bridge network with container DNS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jWfn3RwPfFGTtBddm36Uy
2026-07-28 13:44:46 +02:00
..

Deploy — CTAO portal demo on strapi-experimental.cyfronet

Everything runs rootless as user strapi. Footprint on the machine: ONE directory (~/ctao-portal-demo/) + 4 unit files in ~/.config/. Uninstall restores the machine exactly (see bottom). No secrets in any file.

~/ctao-portal-demo/
├── gitea-data/    # Gitea state (repos, SQLite, accounts)  ← the ONLY thing worth backing up
├── gitea-config/  # Gitea app.ini (generated from env)
├── repo/          # clone of the portal repo (created by first build)
├── releases/      # <sha>/ dirs + `current` symlink (what nginx serves)
├── state/         # last-built SHA
├── bin/build.sh   # copied from deploy/ (source of truth stays in the repo)
└── config/nginx.conf
Port What Exposed how
3000 Gitea ingress vhost (TODO: ask Hubert)
8080 portal (nginx, static) ingress vhost (TODO: ask Hubert)

Install (each step reviewed before running; [W] = writes to the machine)

  1. [W] mkdir -p ~/ctao-portal-demo/{gitea-data,gitea-config,releases,state,bin,config} ~/.config/containers/systemd ~/.config/systemd/user
  2. [W] Copy files from this dir (scp from the Mac):
    • ctao-demo-gitea.container, ctao-demo-web.container, ctao-demo.network → ~/.config/containers/systemd/
    • ctao-portal-build.service, ctao-portal-build.timer → ~/.config/systemd/user/
    • build.sh → ~/ctao-portal-demo/bin/ (chmod +x)
    • nginx.conf → ~/ctao-portal-demo/config/
  3. [W] Pull + pin images (one-time, needs internet): podman pull docker.io/gitea/gitea:1.27-rootless docker.io/library/nginx:stable-alpine docker.io/library/node:24-alpine podman build -t localhost/ctao-portal-build:1 -f Containerfile.build . Then podman images --digests → paste the sha256 digests into both .container files.
  4. [W] systemctl --user daemon-reload && systemctl --user start ctao-demo-gitea
  5. [W] Create the Gitea admin — run YOURSELF in your own terminal (password is prompted/printed there only; never goes through chat or shell history): podman exec -it ctao-demo-gitea gitea admin user create --admin --username <you> --email <you@…> --random-password
  6. [W] In the Gitea UI: create org ctao, repo portal (public read). Push from the Mac through an SSH port-forward: ssh -L 3300:localhost:3000 strapi-experimental.cyfronet then git remote add machine http://localhost:3300/ctao/portal.git && git push machine main
  7. [W] systemctl --user enable --now ctao-portal-build.timer — first run clones + npm ci + builds (minutes); later runs are seconds. Wait until journalctl --user -u ctao-portal-build -n 5 shows published <sha> (starting nginx earlier just serves 404s until the first build lands).
  8. [W] systemctl --user start ctao-demo-web
  9. [R] Verify: curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/ and journalctl --user -u ctao-portal-build -n 20 (shows measured build times).

After Hubert assigns the vhosts

  1. Replace both TODO(vhost) values in ctao-demo-gitea.container (ROOT_URL → gitea vhost, CORS * → portal origin, add GITEA__cors__SCHEME=https); systemctl --user daemon-reload && systemctl --user restart ctao-demo-gitea.
  2. In Gitea UI: Settings → Applications → new OAuth2 app for Sveltia (redirect: https://<portal-vhost>/admin/), PKCE, no client secret.
  3. Put the Gitea vhost URL + client id into public/admin/config.yml in the portal repo, commit, push — the timer publishes it like any other change.

Ask Hubert (one message)

  1. Two ingress vhosts → 192.168.10.15:8080 (portal) and :3000 (gitea) — same mechanism as strapi.isl-dev…:1337.
  2. Are vhosts public-internet or VPN-scopable? (Gitea preferably VPN-only.)
  3. Does Cyfronet offer static-file hosting on the ingress itself? If yes, we drop our nginx container entirely and rsync builds there instead.

Uninstall (leaves zero traces)

systemctl --user disable --now ctao-portal-build.timer
systemctl --user stop ctao-demo-web ctao-demo-gitea
rm ~/.config/containers/systemd/ctao-demo-*.container \
   ~/.config/containers/systemd/ctao-demo.network \
   ~/.config/systemd/user/ctao-portal-build.{service,timer}
systemctl --user daemon-reload
podman network rm ctao-demo 2>/dev/null || true
podman rmi localhost/ctao-portal-build:1 docker.io/gitea/gitea:1.27-rootless docker.io/library/nginx:stable-alpine
rm -rf ~/ctao-portal-demo

Notes

  • Publish latency = poll (≤10 s) + build (measured 1 s on M-series; expect 4–8 s on the 2 vCPU VM — every build's time lands in the journal).
  • Internet needed only for: image pulls (install) and npm ci when the lockfile changes. Routine rebuilds are fully offline.
  • Memory caps (MemoryHigh) keep us polite next to Outline + Strapi; the build container is capped at 1 GB via podman run --memory. Watch the first npm ci + build in the journal — if it OOMs inside its cgroup (contained, just retries), raise the cap.
  • Build isolation: build containers run on the ctao-demo bridge (see ctao-demo.network) — repo/npm code cannot reach host loopback services.
  • Secrets inventory: Gitea admin password (typed interactively, lives only in Gitea's DB) — that's the complete list. Build/poll/serve use none.