# 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/ # -/ 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` 1. Check that `ctao/content:main` contains the four Example files in `pages/` (listed in `src/data/examples.json`) and the uploads they reference. They are already committed there (`a3452d7`, "demo lorem ipsum pages"). News and other page files can remain in the content repository; this portal loads only the four Example files. 2. Push the content branch to Gitea as `ctao/content:main`, and the code branch as `ctao/portal:main`. These are separate pushes; Bitbucket is not the deployment remote. 3. Copy the updated `build.sh` to `~/ctao-portal-demo/bin/build.sh` and the updated `ctao-portal-build.service` to `~/.config/systemd/user/`. Reload systemd with `systemctl --user daemon-reload`. The service explicitly sets `CODE_BRANCH=main` and `CONTENT_BRANCH=main`; the script defaults agree. 4. Start the build service or let its timer run. It overlays `pages/` and `uploads/`, clears the previous news overlay, and runs `npm run check` before the atomic release switch. Missing Example pages fail the build. 5. Verify the four `/example/…/` pages and empty `/news/`. Sign in at `/admin/`, save an Example title, and check that a commit appears on `ctao/content:main` and 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 ` 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) 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 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 --email --random-password` 7. **[W]** In the Gitea UI: create org `ctao` with repos `portal` and `content` (both public read). The content `main` branch needs the four Example pages described above. Then, from your workstation, push both over the vhost with a repo-scoped token: `git push https://:@astro-git.isl-dev.grid.cyfronet.pl/ctao/portal.git main` `git push https://:@astro-git.isl-dev.grid.cyfronet.pl/ctao/content.git main` 8. **[W]** `systemctl --user enable --now ctao-portal-build.timer`. The 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). 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 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 - A release that fails to build is not retried until either repo gets a new commit (`state/last-failed` guards against a 10 s rebuild loop). To force a retry of the SAME release (e.g. after fixing `bin/build.sh` itself): `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 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.