# 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/ # / dirs + `current` symlink (what nginx serves) ├── state/ # last-built SHA ├── npm-cache/ # npm cache for the build container (created by build.sh) ├── bin/build.sh # copied from deploy/ (source of truth stays in the 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) | ## Install (each step reviewed before running; [W] = writes to the machine) 1. **[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`. 2. **[W]** `mkdir -p ~/ctao-portal-demo/{gitea-data,gitea-config,releases,state,bin,config} ~/.config/containers/systemd ~/.config/systemd/user` 3. **[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) 4. **[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:` `podman pull docker.io/library/nginx@sha256:` `podman build -t localhost/ctao-portal-build:1 -f Containerfile.build .` (Upgrading later = pick new digests deliberately, update the pins in the `.container` files / `Containerfile.build`, re-pull, re-build.) 5. **[W]** `systemctl --user daemon-reload && systemctl --user start ctao-demo-gitea` 6. **[W]** Create the Gitea admin — run 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 --email --random-password` 7. **[W]** In the Gitea UI: create org `ctao`, repo `portal` (public read). Then, from your workstation, push the portal repo over the vhost with a repo-scoped token: `git push https://:@astro-git.isl-dev.grid.cyfronet.pl/ctao/portal.git main` 8. **[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 ` (starting nginx earlier just serves 404s until the first build lands). 9. **[W]** `systemctl --user start ctao-demo-web` 10. **[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). ## CMS sign-in (Sveltia ↔ Gitea OAuth) - Gitea OAuth2 app (PKCE, `confidential_client=false`, no secret) with redirect `https://astro.isl-dev.grid.cyfronet.pl/admin/`; its client id is the `app_id` in `public/admin/config.yml`. - CORS is pinned in `ctao-demo-gitea.container`: `ALLOW_DOMAIN` takes the FULL portal origin with scheme (a bare hostname silently disables CORS in Gitea 1.27; there is no `SCHEME` key) and `HEADERS` must include `Authorization` or 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`, then `systemctl --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 - **Only site content auto-deploys.** 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 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 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: no `ip_tables` kernel 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.