diff --git a/deploy/Containerfile.build b/deploy/Containerfile.build index 19f43ab..3314604 100644 --- a/deploy/Containerfile.build +++ b/deploy/Containerfile.build @@ -3,5 +3,5 @@ # keeps the whole toolchain pinned and independent of host packages. # Built ONCE at install, never pulled again at runtime: # podman build -t localhost/ctao-portal-build:1 -f Containerfile.build . -FROM docker.io/library/node:24-alpine +FROM docker.io/library/node:24-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd RUN apk add --no-cache git diff --git a/deploy/README.md b/deploy/README.md index 0dc1295..6891a54 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -24,7 +24,7 @@ Uninstall restores the machine exactly (see bottom). No secrets in any file. 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-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/` @@ -72,10 +72,8 @@ Uninstall restores the machine exactly (see bottom). No secrets in any file. 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 ``` @@ -90,7 +88,9 @@ rm -rf ~/ctao-portal-demo 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. +- 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) — that's the complete list. Build/poll/serve use none. diff --git a/deploy/build.sh b/deploy/build.sh index d7f8fc5..f2ad66d 100755 --- a/deploy/build.sh +++ b/deploy/build.sh @@ -12,10 +12,12 @@ GITEA_URL="${GITEA_URL:-http://localhost:3000}" # published by ctao-demo-gitea REPO="${REPO:-ctao/portal}" # owner/repo in Gitea BRANCH="${BRANCH:-main}" BUILD_IMAGE="${BUILD_IMAGE:-localhost/ctao-portal-build:1}" -BUILD_NET="${BUILD_NET:-ctao-demo}" # bridge from ctao-demo.network -# Clone URL as seen from INSIDE the build network (container-name DNS); -# the poll below uses $GITEA_URL because it runs on the host. -REPO_INTERNAL="${REPO_INTERNAL:-http://ctao-demo-gitea:3000/$REPO.git}" +# The build joins the Gitea container's network namespace: localhost inside +# the build = Gitea's loopback (port 3000), host loopback stays unreachable. +# (A netavark bridge would be equivalent, but rootless bridges need the +# ip_tables kernel module, absent on the machine — pasta needs nothing.) +BUILD_NETNS="${BUILD_NETNS:-container:ctao-demo-gitea}" +REPO_INTERNAL="${REPO_INTERNAL:-http://localhost:3000/$REPO.git}" KEEP="${KEEP:-3}" # released builds to retain mkdir -p "$BASE/repo" "$BASE/releases" "$BASE/state" @@ -35,11 +37,11 @@ t0=$(date +%s) # --- 2. Build in the ephemeral container (git + pinned node live there). # SECURITY: the container runs npm lifecycle scripts from the repo, so it is -# confined to the ctao-demo bridge — it reaches Gitea by container name and -# the internet (for `npm ci` when the lockfile changed), but NOT the host's -# loopback services (code-server, Strapi). Never use --network=host here. +# confined to Gitea's netns — it reaches Gitea on localhost:3000 and the +# internet (for `npm ci` when the lockfile changed), but NOT the host's +# loopback services (code-server). Never use --network=host here. # node_modules and .deps-hash are untracked, so they survive checkouts. -podman run --rm --network="$BUILD_NET" --memory=1g \ +podman run --rm --network="$BUILD_NETNS" --memory=1g \ -e ASTRO_TELEMETRY_DISABLED=1 \ -e SHA="$sha" -e REPO_URL="$REPO_INTERNAL" -e BRANCH="$BRANCH" \ -v "$BASE/repo:/work/repo:z" \ diff --git a/deploy/ctao-demo-gitea.container b/deploy/ctao-demo-gitea.container index 44b9481..274390e 100644 --- a/deploy/ctao-demo-gitea.container +++ b/deploy/ctao-demo-gitea.container @@ -11,16 +11,15 @@ After=network-online.target [Container] ContainerName=ctao-demo-gitea -# Same major as the local demo (1.27). Re-pin to the exact digest at install: -# podman pull docker.io/gitea/gitea:1.27-rootless -# podman images --digests docker.io/gitea/gitea → paste sha256 here -Image=docker.io/gitea/gitea:1.27-rootless +# Pinned by digest at install (2026-07-28); tag kept for readability. +Image=docker.io/gitea/gitea:1.27-rootless@sha256:36cce26be71609091e1236d5b5de2c66a81fb8a7d45756a5fd3b7a28c11733b7 # The rootless image runs as uid 1000 inside; keep-id maps it to the host user # so the bind-mounted dirs stay owned by `strapi` (no chown, no root anywhere). UserNS=keep-id:uid=1000,gid=1000 -# Shared bridge with the build containers (name-resolvable as ctao-demo-gitea); -# host loopback stays out of reach for build code — see ctao-demo.network. -Network=ctao-demo.network +# Networking: default rootless pasta (userspace — the machine's kernel lacks +# ip_tables for rootless netavark bridges, and we have no sudo). The build +# container joins THIS container's netns (--network=container:ctao-demo-gitea), +# so it sees Gitea on localhost:3000 while the HOST loopback stays invisible. Volume=%h/ctao-portal-demo/gitea-data:/var/lib/gitea:Z Volume=%h/ctao-portal-demo/gitea-config:/etc/gitea:Z PublishPort=3000:3000 @@ -37,6 +36,10 @@ Environment=GITEA__database__DB_TYPE=sqlite3 Environment=GITEA__security__INSTALL_LOCK=true Environment=GITEA__service__DISABLE_REGISTRATION=true Environment=GITEA__mailer__ENABLED=false +# First `git push` auto-creates the repo (no UI step); public so the build +# pipeline can clone anonymously — content is the public site anyway. +Environment=GITEA__repository__ENABLE_PUSH_CREATE_USER=true +Environment=GITEA__repository__DEFAULT_PRIVATE=public # Sveltia is served from the portal vhost and calls the Gitea API cross-origin. # TODO(vhost): tighten * to the portal origin once known, and add # GITEA__cors__SCHEME=https (the [cors] SCHEME default is http). diff --git a/deploy/ctao-demo-web.container b/deploy/ctao-demo-web.container index 8c29a32..dd1355d 100644 --- a/deploy/ctao-demo-web.container +++ b/deploy/ctao-demo-web.container @@ -9,8 +9,8 @@ After=network-online.target [Container] ContainerName=ctao-demo-web -# Official image; re-pin to the exact digest at install (same drill as Gitea). -Image=docker.io/library/nginx:stable-alpine +# Pinned by digest at install (2026-07-28); tag kept for readability. +Image=docker.io/library/nginx:stable-alpine@sha256:97d490c12ba55b4946b01546d1c3ed324e8d41ab1c9fcb2a616aa470620e5b46 # releases/ is shared with the build container (:z shared label); # the config file is exclusive to nginx (:Z). Volume=%h/ctao-portal-demo/releases:/srv/releases:ro,z diff --git a/deploy/ctao-demo.network b/deploy/ctao-demo.network deleted file mode 100644 index 1fe66ed..0000000 --- a/deploy/ctao-demo.network +++ /dev/null @@ -1,11 +0,0 @@ -# CTAO portal demo — dedicated bridge network (quadlet .network unit). -# Why it exists: the build container executes npm lifecycle scripts from the -# repo, so it must NOT see the host's loopback (code-server, Strapi). On this -# bridge, containers resolve each other by name (aardvark-dns) and have NATed -# internet egress, but host 127.0.0.1 services are unreachable — unlike -# --network=host, which this replaces. -[Unit] -Description=CTAO portal demo — internal bridge network - -[Network] -NetworkName=ctao-demo