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.
Two Gitea repos by design: ctao/portal (code, developed by the team) and
ctao/content (Markdown + uploads, committed by the CMS). The build overlays
content onto code; a push to EITHER republishes the site.
~/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 code repo (created by first build)
├── content/ # clone of the content repo (created by first build)
├── releases/ # <code>-<content>/ dirs + `current` symlink (what nginx serves)
├── state/ # last-built release id
├── npm-cache/ # npm cache for the build container (created by build.sh)
├── bin/build.sh # copied from deploy/ (source of truth stays in the code repo)
└── config/nginx.conf
Host prerequisites: podman, curl, jq (build.sh checks and says which is
missing). Everything else runs inside containers.
| Port | What | Exposed as |
|---|---|---|
| 3000 | Gitea | https://astro-git.isl-dev.grid.cyfronet.pl (ingress vhost) |
| 8080 | portal (nginx, static) | https://astro.isl-dev.grid.cyfronet.pl (ingress vhost) |
Publish the simplified portal from main
- In a checkout of
ctao/content, switch tomain. Copy the missing Example files fromsample-content/pages/andsample-content/uploads/hero.jpgfrom this code repository into its rootpages/anduploads/directories, then commit the seed files. Preserve any existing editorial versions of these files. Existing news or other page files can remain in the content history; this portal loads only the four Example files. - Push the content branch to Gitea as
ctao/content:main, and the code branch asctao/portal:main. These are separate pushes; Bitbucket is not the deployment remote. - Copy the updated
build.shto~/ctao-portal-demo/bin/build.shand the updatedctao-portal-build.serviceto~/.config/systemd/user/. Reload systemd withsystemctl --user daemon-reload. The service explicitly setsCODE_BRANCH=mainandCONTENT_BRANCH=main; the script defaults agree. - Start the build service or let its timer run. It overlays
pages/anduploads/, clears the previous news overlay, and runsnpm run checkbefore the atomic release switch. Missing Example pages fail the build. - Verify the four
/example/…/pages and empty/news/. Sign in at/admin/, save an Example title, and check that a commit appears onctao/content:mainand the rebuilt page reflects it.
The OAuth client and public portal origin stay the same. Sveltia's
backend.branch in public/admin/config.yml must always match
CONTENT_BRANCH. Branches are selected independently; a single BRANCH
environment variable is no longer used. For a separate preview hostname,
update the site origin, OAuth redirect, and Gitea CORS together.
To undo the simplification in shared code history, use
git revert -m 1 <simplification-merge-commit> on main and publish the
resulting commit. The previous portal code is also preserved at 0772462.
For an immediate deployment rollback, restore the previous release and worker
script/service configuration. Content has its own history: reverting a code
commit does not revert CMS edits. Keep the old content files available if
the previous portal may be restored.
Install (each step reviewed before running; [W] = writes to the machine)
- [W]
loginctl enable-linger $USER. Without lingering every user unit dies at logout and nothing starts after a reboot. Verify:loginctl show-user $USER -p Linger→Linger=yes. - [W]
mkdir -p ~/ctao-portal-demo/{gitea-data,gitea-config,releases,state,bin,config} ~/.config/containers/systemd ~/.config/systemd/user - [W] Copy files from this dir (scp):
ctao-demo-gitea.container,ctao-demo-web.container→~/.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/Containerfile.build→ anywhere (needed once, for the next step)
- [W] Pull the images at the digests pinned in the unit files (one-time,
needs internet) and build the build image:
podman pull docker.io/gitea/gitea@sha256:<digest from ctao-demo-gitea.container>podman pull docker.io/library/nginx@sha256:<digest from ctao-demo-web.container>podman build -t localhost/ctao-portal-build:1 -f Containerfile.build .(Upgrading later = pick new digests deliberately, update the pins in the.containerfiles /Containerfile.build, re-pull, re-build.) - [W]
systemctl --user daemon-reload && systemctl --user start ctao-demo-gitea - [W] Create the Gitea admin. Run it interactively in a terminal so the
password never lands in a file or shell history:
podman exec -it ctao-demo-gitea gitea admin user create --admin --username <you> --email <you@…> --random-password - [W] In the Gitea UI: create org
ctaowith reposportalandcontent(both public read). Seed the contentmainbranch withsample-content/pages/andsample-content/uploads/as described above. Then, from your workstation, push both over the vhost with a repo-scoped token:git push https://<user>:<token>@astro-git.isl-dev.grid.cyfronet.pl/ctao/portal.git maingit push https://<user>:<token>@astro-git.isl-dev.grid.cyfronet.pl/ctao/content.git main - [W]
systemctl --user enable --now ctao-portal-build.timer. The first run clones +npm ci+ builds (minutes); later runs are seconds. Wait untiljournalctl --user -u ctao-portal-build -n 5showspublished <sha>(starting nginx earlier just serves 404s until the first build lands). - [W]
systemctl --user start ctao-demo-web - [R] Verify:
curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/andjournalctl --user -u ctao-portal-build -n 20(shows measured build times).
Editor accounts need WRITE access to ctao/content (Gitea → repo →
Collaborators, or a team); public read alone lets them sign in but every
save fails.
CMS sign-in (Sveltia ↔ Gitea OAuth)
- Gitea OAuth2 app (PKCE,
confidential_client=false, no secret) with redirecthttps://astro.isl-dev.grid.cyfronet.pl/admin/; its client id is theapp_idinpublic/admin/config.yml. - CORS is pinned in
ctao-demo-gitea.container:ALLOW_DOMAINtakes the FULL portal origin with scheme (a bare hostname silently disables CORS in Gitea 1.27; there is noSCHEMEkey) andHEADERSmust includeAuthorizationor authenticated API calls from the browser fail. - OAuth requires a secure context: the vhosts must stay HTTPS.
- Changing origins later = edit the quadlet env + the OAuth app's redirect
URI +
config.yml, thensystemctl --user daemon-reload && systemctl --user restart ctao-demo-gitea.
Uninstall (leaves only podman's own storage metadata)
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/systemd/user/ctao-portal-build.{service,timer}
systemctl --user daemon-reload
podman rmi localhost/ctao-portal-build:1 docker.io/gitea/gitea:1.27-rootless \
docker.io/library/nginx:stable-alpine docker.io/library/node:24-alpine
rm -rf ~/ctao-portal-demo
loginctl disable-linger $USER # only if nothing else of yours should survive logout
Notes
-
A release that fails to build is not retried until either repo gets a new commit (
state/last-failedguards against a 10 s rebuild loop). To force a retry of the SAME release (e.g. after fixingbin/build.shitself):rm ~/ctao-portal-demo/state/last-failed. -
Only pushes to the machine's Gitea auto-deploy (code and content repos alike). Changes to
deploy/*need a manual re-copy:build.sh→bin/,nginx.conf→config/+systemctl --user restart ctao-demo-web, unit files →~/.config/…+systemctl --user daemon-reload(+ restart). This is deliberate: the build pipeline must not execute host-side code straight from the content repo. -
Publish latency = poll (≤10 s) + build (measured: astro build 5–7 s on the 2 vCPU VM, whole build.sh 16–21 s; every build's time lands in the journal).
-
Internet needed only for: image pulls (install) and
npm ciwhen 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 viapodman run --memory. Watch the firstnpm ci+ build in the journal; if it OOMs inside its cgroup (contained, just retries), raise the cap. -
Build isolation: build containers join the Gitea container's network namespace (
--network=container:ctao-demo-gitea): repo/npm code sees Gitea on localhost:3000 but cannot reach host loopback services. (Rootless netavark bridges don't work here: noip_tableskernel module, no sudo.) -
Secrets inventory: Gitea admin password (typed interactively, lives only in Gitea's DB) and repo-scoped push tokens (managed in Gitea); that's the complete list. Build/poll/serve use none.