deploy: join gitea netns instead of bridge (machine kernel lacks ip_tables for rootless netavark); pin image digests; push-to-create repo

This commit is contained in:
2026-07-28 14:07:44 +02:00
parent ed7a9c01e7
commit 80d886c0cb
6 changed files with 28 additions and 34 deletions
+1 -1
View File
@@ -3,5 +3,5 @@
# keeps the whole toolchain pinned and independent of host packages. # keeps the whole toolchain pinned and independent of host packages.
# Built ONCE at install, never pulled again at runtime: # Built ONCE at install, never pulled again at runtime:
# podman build -t localhost/ctao-portal-build:1 -f Containerfile.build . # 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 RUN apk add --no-cache git
+5 -5
View File
@@ -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` 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): 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/` - `ctao-portal-build.service`, `ctao-portal-build.timer` → `~/.config/systemd/user/`
- `build.sh` → `~/ctao-portal-demo/bin/` (`chmod +x`) - `build.sh` → `~/ctao-portal-demo/bin/` (`chmod +x`)
- `nginx.conf` → `~/ctao-portal-demo/config/` - `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 disable --now ctao-portal-build.timer
systemctl --user stop ctao-demo-web ctao-demo-gitea systemctl --user stop ctao-demo-web ctao-demo-gitea
rm ~/.config/containers/systemd/ctao-demo-*.container \ rm ~/.config/containers/systemd/ctao-demo-*.container \
~/.config/containers/systemd/ctao-demo.network \
~/.config/systemd/user/ctao-portal-build.{service,timer} ~/.config/systemd/user/ctao-portal-build.{service,timer}
systemctl --user daemon-reload 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 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 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 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 first `npm ci` + build in the journal — if it OOMs inside its cgroup
(contained, just retries), raise the cap. (contained, just retries), raise the cap.
- Build isolation: build containers run on the `ctao-demo` bridge (see - Build isolation: build containers join the Gitea container's network
`ctao-demo.network`) — repo/npm code cannot reach host loopback services. 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 - Secrets inventory: Gitea admin password (typed interactively, lives only
in Gitea's DB) — that's the complete list. Build/poll/serve use none. in Gitea's DB) — that's the complete list. Build/poll/serve use none.
+10 -8
View File
@@ -12,10 +12,12 @@ GITEA_URL="${GITEA_URL:-http://localhost:3000}" # published by ctao-demo-gitea
REPO="${REPO:-ctao/portal}" # owner/repo in Gitea REPO="${REPO:-ctao/portal}" # owner/repo in Gitea
BRANCH="${BRANCH:-main}" BRANCH="${BRANCH:-main}"
BUILD_IMAGE="${BUILD_IMAGE:-localhost/ctao-portal-build:1}" BUILD_IMAGE="${BUILD_IMAGE:-localhost/ctao-portal-build:1}"
BUILD_NET="${BUILD_NET:-ctao-demo}" # bridge from ctao-demo.network # The build joins the Gitea container's network namespace: localhost inside
# Clone URL as seen from INSIDE the build network (container-name DNS); # the build = Gitea's loopback (port 3000), host loopback stays unreachable.
# the poll below uses $GITEA_URL because it runs on the host. # (A netavark bridge would be equivalent, but rootless bridges need the
REPO_INTERNAL="${REPO_INTERNAL:-http://ctao-demo-gitea:3000/$REPO.git}" # 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 KEEP="${KEEP:-3}" # released builds to retain
mkdir -p "$BASE/repo" "$BASE/releases" "$BASE/state" 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). # --- 2. Build in the ephemeral container (git + pinned node live there).
# SECURITY: the container runs npm lifecycle scripts from the repo, so it is # 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 # confined to Gitea's netns — it reaches Gitea on localhost:3000 and the
# the internet (for `npm ci` when the lockfile changed), but NOT the host's # internet (for `npm ci` when the lockfile changed), but NOT the host's
# loopback services (code-server, Strapi). Never use --network=host here. # loopback services (code-server). Never use --network=host here.
# node_modules and .deps-hash are untracked, so they survive checkouts. # 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 ASTRO_TELEMETRY_DISABLED=1 \
-e SHA="$sha" -e REPO_URL="$REPO_INTERNAL" -e BRANCH="$BRANCH" \ -e SHA="$sha" -e REPO_URL="$REPO_INTERNAL" -e BRANCH="$BRANCH" \
-v "$BASE/repo:/work/repo:z" \ -v "$BASE/repo:/work/repo:z" \
+10 -7
View File
@@ -11,16 +11,15 @@ After=network-online.target
[Container] [Container]
ContainerName=ctao-demo-gitea ContainerName=ctao-demo-gitea
# Same major as the local demo (1.27). Re-pin to the exact digest at install: # Pinned by digest at install (2026-07-28); tag kept for readability.
# podman pull docker.io/gitea/gitea:1.27-rootless Image=docker.io/gitea/gitea:1.27-rootless@sha256:36cce26be71609091e1236d5b5de2c66a81fb8a7d45756a5fd3b7a28c11733b7
# podman images --digests docker.io/gitea/gitea → paste sha256 here
Image=docker.io/gitea/gitea:1.27-rootless
# The rootless image runs as uid 1000 inside; keep-id maps it to the host user # 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). # so the bind-mounted dirs stay owned by `strapi` (no chown, no root anywhere).
UserNS=keep-id:uid=1000,gid=1000 UserNS=keep-id:uid=1000,gid=1000
# Shared bridge with the build containers (name-resolvable as ctao-demo-gitea); # Networking: default rootless pasta (userspace — the machine's kernel lacks
# host loopback stays out of reach for build code — see ctao-demo.network. # ip_tables for rootless netavark bridges, and we have no sudo). The build
Network=ctao-demo.network # 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-data:/var/lib/gitea:Z
Volume=%h/ctao-portal-demo/gitea-config:/etc/gitea:Z Volume=%h/ctao-portal-demo/gitea-config:/etc/gitea:Z
PublishPort=3000:3000 PublishPort=3000:3000
@@ -37,6 +36,10 @@ Environment=GITEA__database__DB_TYPE=sqlite3
Environment=GITEA__security__INSTALL_LOCK=true Environment=GITEA__security__INSTALL_LOCK=true
Environment=GITEA__service__DISABLE_REGISTRATION=true Environment=GITEA__service__DISABLE_REGISTRATION=true
Environment=GITEA__mailer__ENABLED=false 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. # 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 # TODO(vhost): tighten * to the portal origin once known, and add
# GITEA__cors__SCHEME=https (the [cors] SCHEME default is http). # GITEA__cors__SCHEME=https (the [cors] SCHEME default is http).
+2 -2
View File
@@ -9,8 +9,8 @@ After=network-online.target
[Container] [Container]
ContainerName=ctao-demo-web ContainerName=ctao-demo-web
# Official image; re-pin to the exact digest at install (same drill as Gitea). # Pinned by digest at install (2026-07-28); tag kept for readability.
Image=docker.io/library/nginx:stable-alpine Image=docker.io/library/nginx:stable-alpine@sha256:97d490c12ba55b4946b01546d1c3ed324e8d41ab1c9fcb2a616aa470620e5b46
# releases/ is shared with the build container (:z shared label); # releases/ is shared with the build container (:z shared label);
# the config file is exclusive to nginx (:Z). # the config file is exclusive to nginx (:Z).
Volume=%h/ctao-portal-demo/releases:/srv/releases:ro,z Volume=%h/ctao-portal-demo/releases:/srv/releases:ro,z
-11
View File
@@ -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