Publish multi-arch homelab-fabric container image #1

Merged
Nimby merged 1 commit from container-image into main 2026-10-09 17:42:32 -04:00
Collaborator

Builds and publishes the Fabric dashboard as a container image, so homelab's Ansible can pull it (like astr and comfy-gallery) instead of running docker compose build on channel66.

⚠️ Blocked on a runner change before merge. The runner job image act-runner-docker-cli has no docker buildx (details below). As written, build-and-push will fail at its first step with a clear error until that's fixed in homelab. The test job works today. I did not fall back to an amd64-only build.

Image

git.scruffydude.net/scruffydude/homelab-fabric, a multi-arch manifest list (linux/amd64, linux/arm64)

Tag When
:<short-sha> every build
:latest push to main
:vX.Y.Z push of a v* tag (this is how releases are cut)

:latest is gated on github.ref == refs/heads/main, not ref_type == branch, so a workflow_dispatch on another branch can't move latest. Otherwise the tagging, login and logout steps are the same as astr and comfy-gallery.

Changes

  • Dockerfile (root, not templated): python:3.12-slim-bookworm + openssh-client. Every fabric/*.py goes in /app, so ai_fabric_dispatch.py sits beside ai_fabric.py. ENTRYPOINT python3 /app/ai_fabric.py, CMD serve, EXPOSE 8765. The HEALTHCHECK is written in Python and requests /api/summary with Remote-Groups: admins, because the endpoint returns 403 without it. No secrets, hostnames or IPs.
  • fabric/ai_fabric.py: every serve flag now defaults from an env var, and explicit flags still override. --import-before-serve became --[no-]import-before-serve so the CLI can turn off an env-enabled value. Bad env values fail with an argparse error. Defaults with no env set are unchanged (covered by tests).
  • .dockerignore (tests, docs, .git, .forgejo, caches).
  • .forgejo/workflows/build.yml replaces ci.yml. The test job (pytest + sample-job contract check) runs on PRs and pushes. build-and-push (needs: test, push/workflow_dispatch only) runs these steps in order:
    1. check for buildx
    2. log in
    3. tonistiigi/binfmt:qemu-v10.2.3 --install arm64
    4. create a per-run docker-container buildx builder
    5. build amd64 with --load and run the smoke test
    6. buildx build --platform linux/amd64,linux/arm64 --push with all tags
    7. imagetools inspect each tag
    8. if: always() cleanup of the builder and smoke image, then logout
  • tests/smoke_image.sh IMAGE: starts the image with its default ENV/CMD, a temp /data and a minimal /repo built from tests/fixtures. It waits for health healthy, then asserts 200 from /api/summary and /api/jobs and that the fixture worker and job were loaded. Fixtures go in with docker cp rather than bind mounts, because the CI job container drives the host daemon through the socket and its paths don't exist on the host.
  • README: image, tags, env vars, mounts, release process.

Env vars (image defaults = today's deployment)

Variable Flag Image default
AI_FABRIC_HOST --host 0.0.0.0
AI_FABRIC_PORT --port 8765
AI_FABRIC_DB --db /data/fabric.sqlite3
AI_FABRIC_ARTIFACT_ROOT --artifact-root /data/agent-runs
AI_FABRIC_HOST_CONFIG --host-config /data/worker-registry.json
AI_FABRIC_JOB_DIR --job-dir /repo/examples/agent-jobs
AI_FABRIC_REPO_ROOT --repo-root /repo
AI_FABRIC_IMPORT_BEFORE_SERVE --[no-]import-before-serve true
AI_FABRIC_IMPORT_INTERVAL --import-interval 15
AI_FABRIC_COST_PER_MILLION_TOKENS --cost-per-million-tokens 10

Mounts

Path Mode Contents
/data rw sqlite index, agent-runs/, worker-registry.json (must be seeded; the server exits if it's missing, same as today)
/repo ro consumer checkout: scripts/agents/agent_runner.py, examples/agent-jobs, examples/agent-hosts
/root/.ssh ro dispatch key + known_hosts

Unchanged contract: dispatch still runs <repo-root>/scripts/agents/agent_runner.py with cwd=<repo-root> from the mounted consumer checkout.

Multi-arch approach and runner changes

The build cross-builds on the x86_64 runner with buildx (docker-container driver) and QEMU. Both run on the host daemon through the mounted socket, so the job container itself doesn't need to be privileged. binfmt registration is kernel-wide on optibcc and persists until reboot. Re-running the registration each build is idempotent.

I checked the runner on optibcc: act-runner-docker-cli ships Debian bookworm docker.io 20.10.24 with no CLI plugins, and docker buildx version fails with "'buildx' is not a docker command". Neither docker-buildx nor docker-buildx-plugin exists in its apt sources (bookworm main only). The optibcc host itself has buildx v0.36.1, but jobs can't see it.

Needed in homelab ansible/roles/act_runner (pick one):

  • Install the buildx CLI plugin binary into the job image: download buildx-vX.Y.Z.linux-amd64 from github.com/docker/buildx releases to /usr/libexec/docker/cli-plugins/docker-buildx and chmod +x it, or
  • Switch the image from Debian docker.io to Docker's apt repo (docker-ce-cli + docker-buildx-plugin).

Then rebuild act-runner-docker-cli. No change to the runner's privileged or socket config is needed.

Verification

  • pytest tests/: 27 passed, including 4 new tests: env defaults, no-env defaults unchanged, flags override env, bad env rejected.
  • Image built and tests/smoke_image.sh passed natively on both arches: amd64 on optibcc, arm64 on deskpi (aarch64, same arch as channel66). Results: health: healthy, GET /api/summary: 200, GET /api/jobs: 200, fixture worker registry and job spec loaded. A negative run with a corrupt worker-registry.json made the script fail as expected. Temp images and dirs were removed afterwards.
  • I did not run the cross-built (QEMU) arm64 path end to end. That needs buildx in the job image, and I didn't register binfmt on optibcc by hand.
  • actionlint (with shellcheck) is clean on build.yml, apart from the expected unknown-label notice for the custom docker label. shellcheck is clean on smoke_image.sh.

Required before the first build

  • REGISTRY_TOKEN repo Actions secret. I couldn't check whether it exists, because listing secrets needs repo-owner access. If it's missing, add a scruffydude access token with write:package, the same as astr and comfy-gallery.
  • buildx in the runner job image (above).

Homelab follow-ups (not done here; homelab untouched)

  1. ai_fabric role: pull, don't build.
    • Drop templates/Dockerfile.j2, the "Copy AI Fabric application files" task, base_dir/app, the compose build: block and the "Build AI Fabric image" task.
    • Set ai_fabric.image: git.scruffydude.net/scruffydude/homelab-fabric:vX.Y.Z (pin a release tag rather than latest) and pull it, e.g. docker_compose_v2 with pull: always/missing.
    • channel66 needs registry pull access if the package is private.
  2. Config via env: the image defaults already match today's CMD. Only set environment: in compose where values differ, e.g. AI_FABRIC_COST_PER_MILLION_TOKENS (the old template defaulted to 0.0; this image defaults to 10 per the spec) and AI_FABRIC_IMPORT_INTERVAL. Keep ai_fabric.internal_port at 8765 or set AI_FABRIC_PORT to match. Mounts are unchanged; keep init: true.
  3. Existing bug noticed: the role's "Wait for AI Fabric HTTP endpoint" task GETs /api/summary with no Remote-Groups header. Since non-admins were restricted (af94603) that endpoint returns 403, so the task can't reach 200. Add headers: {Remote-Groups: admins} or probe /jobs. Alternatively, wait on the container's health status, which the image now provides.
  4. act_runner: add buildx to act-runner-docker-cli (above).
  5. Update homelab's release flow: after scripts/sync_fabric.sh vX.Y.Z, also bump the image tag.

🤖 Generated with Claude Code

Builds and publishes the Fabric dashboard as a container image, so homelab's Ansible can pull it (like astr and comfy-gallery) instead of running `docker compose build` on channel66. > ⚠️ **Blocked on a runner change before merge.** The runner job image `act-runner-docker-cli` has **no `docker buildx`** (details below). As written, `build-and-push` will fail at its first step with a clear error until that's fixed in homelab. The `test` job works today. I did **not** fall back to an amd64-only build. ## Image `git.scruffydude.net/scruffydude/homelab-fabric`, a multi-arch manifest list (`linux/amd64`, `linux/arm64`) | Tag | When | | --- | --- | | `:<short-sha>` | every build | | `:latest` | push to `main` | | `:vX.Y.Z` | push of a `v*` tag (this is how releases are cut) | `:latest` is gated on `github.ref == refs/heads/main`, not `ref_type == branch`, so a `workflow_dispatch` on another branch can't move `latest`. Otherwise the tagging, login and logout steps are the same as astr and comfy-gallery. ## Changes - **`Dockerfile`** (root, not templated): `python:3.12-slim-bookworm` + `openssh-client`. Every `fabric/*.py` goes in `/app`, so `ai_fabric_dispatch.py` sits beside `ai_fabric.py`. `ENTRYPOINT python3 /app/ai_fabric.py`, `CMD serve`, `EXPOSE 8765`. The `HEALTHCHECK` is written in Python and requests `/api/summary` with `Remote-Groups: admins`, because the endpoint returns 403 without it. No secrets, hostnames or IPs. - **`fabric/ai_fabric.py`**: every `serve` flag now defaults from an env var, and explicit flags still override. `--import-before-serve` became `--[no-]import-before-serve` so the CLI can turn off an env-enabled value. Bad env values fail with an argparse error. Defaults with no env set are unchanged (covered by tests). - **`.dockerignore`** (tests, docs, .git, .forgejo, caches). - **`.forgejo/workflows/build.yml`** replaces `ci.yml`. The `test` job (pytest + sample-job contract check) runs on PRs and pushes. `build-and-push` (`needs: test`, push/workflow_dispatch only) runs these steps in order: 1. check for buildx 2. log in 3. `tonistiigi/binfmt:qemu-v10.2.3 --install arm64` 4. create a per-run `docker-container` buildx builder 5. build amd64 with `--load` and run the smoke test 6. `buildx build --platform linux/amd64,linux/arm64 --push` with all tags 7. `imagetools inspect` each tag 8. `if: always()` cleanup of the builder and smoke image, then logout - **`tests/smoke_image.sh IMAGE`**: starts the image with its default ENV/CMD, a temp `/data` and a minimal `/repo` built from `tests/fixtures`. It waits for health `healthy`, then asserts 200 from `/api/summary` and `/api/jobs` and that the fixture worker and job were loaded. Fixtures go in with `docker cp` rather than bind mounts, because the CI job container drives the host daemon through the socket and its paths don't exist on the host. - **README**: image, tags, env vars, mounts, release process. ## Env vars (image defaults = today's deployment) | Variable | Flag | Image default | | --- | --- | --- | | `AI_FABRIC_HOST` | `--host` | `0.0.0.0` | | `AI_FABRIC_PORT` | `--port` | `8765` | | `AI_FABRIC_DB` | `--db` | `/data/fabric.sqlite3` | | `AI_FABRIC_ARTIFACT_ROOT` | `--artifact-root` | `/data/agent-runs` | | `AI_FABRIC_HOST_CONFIG` | `--host-config` | `/data/worker-registry.json` | | `AI_FABRIC_JOB_DIR` | `--job-dir` | `/repo/examples/agent-jobs` | | `AI_FABRIC_REPO_ROOT` | `--repo-root` | `/repo` | | `AI_FABRIC_IMPORT_BEFORE_SERVE` | `--[no-]import-before-serve` | `true` | | `AI_FABRIC_IMPORT_INTERVAL` | `--import-interval` | `15` | | `AI_FABRIC_COST_PER_MILLION_TOKENS` | `--cost-per-million-tokens` | `10` | ## Mounts | Path | Mode | Contents | | --- | --- | --- | | `/data` | rw | sqlite index, `agent-runs/`, `worker-registry.json` (must be seeded; the server exits if it's missing, same as today) | | `/repo` | ro | consumer checkout: `scripts/agents/agent_runner.py`, `examples/agent-jobs`, `examples/agent-hosts` | | `/root/.ssh` | ro | dispatch key + `known_hosts` | Unchanged contract: dispatch still runs `<repo-root>/scripts/agents/agent_runner.py` with `cwd=<repo-root>` from the mounted consumer checkout. ## Multi-arch approach and runner changes The build cross-builds on the x86_64 runner with buildx (`docker-container` driver) and QEMU. Both run on the **host** daemon through the mounted socket, so the job container itself doesn't need to be privileged. binfmt registration is kernel-wide on optibcc and persists until reboot. Re-running the registration each build is idempotent. **I checked the runner on optibcc:** `act-runner-docker-cli` ships Debian bookworm `docker.io` 20.10.24 with no CLI plugins, and `docker buildx version` fails with "'buildx' is not a docker command". Neither `docker-buildx` nor `docker-buildx-plugin` exists in its apt sources (bookworm main only). The optibcc host itself has buildx v0.36.1, but jobs can't see it. **Needed in homelab `ansible/roles/act_runner`** (pick one): - Install the buildx CLI plugin binary into the job image: download `buildx-vX.Y.Z.linux-amd64` from github.com/docker/buildx releases to `/usr/libexec/docker/cli-plugins/docker-buildx` and `chmod +x` it, **or** - Switch the image from Debian `docker.io` to Docker's apt repo (`docker-ce-cli` + `docker-buildx-plugin`). Then rebuild `act-runner-docker-cli`. No change to the runner's privileged or socket config is needed. ## Verification - `pytest tests/`: 27 passed, including 4 new tests: env defaults, no-env defaults unchanged, flags override env, bad env rejected. - Image built and `tests/smoke_image.sh` passed **natively on both arches**: amd64 on optibcc, arm64 on deskpi (aarch64, same arch as channel66). Results: `health: healthy`, `GET /api/summary: 200`, `GET /api/jobs: 200`, fixture worker registry and job spec loaded. A negative run with a corrupt `worker-registry.json` made the script fail as expected. Temp images and dirs were removed afterwards. - I did **not** run the cross-built (QEMU) arm64 path end to end. That needs buildx in the job image, and I didn't register binfmt on optibcc by hand. - `actionlint` (with shellcheck) is clean on `build.yml`, apart from the expected unknown-label notice for the custom `docker` label. `shellcheck` is clean on `smoke_image.sh`. ## Required before the first build - [ ] **`REGISTRY_TOKEN` repo Actions secret.** I couldn't check whether it exists, because listing secrets needs repo-owner access. If it's missing, add a `scruffydude` access token with `write:package`, the same as astr and comfy-gallery. - [ ] buildx in the runner job image (above). ## Homelab follow-ups (not done here; homelab untouched) 1. **`ai_fabric` role: pull, don't build.** - Drop `templates/Dockerfile.j2`, the "Copy AI Fabric application files" task, `base_dir/app`, the compose `build:` block and the "Build AI Fabric image" task. - Set `ai_fabric.image: git.scruffydude.net/scruffydude/homelab-fabric:vX.Y.Z` (pin a release tag rather than `latest`) and pull it, e.g. `docker_compose_v2` with `pull: always`/`missing`. - channel66 needs registry pull access if the package is private. 2. **Config via env:** the image defaults already match today's CMD. Only set `environment:` in compose where values differ, e.g. `AI_FABRIC_COST_PER_MILLION_TOKENS` (the old template defaulted to `0.0`; this image defaults to `10` per the spec) and `AI_FABRIC_IMPORT_INTERVAL`. Keep `ai_fabric.internal_port` at 8765 or set `AI_FABRIC_PORT` to match. Mounts are unchanged; keep `init: true`. 3. **Existing bug noticed:** the role's "Wait for AI Fabric HTTP endpoint" task GETs `/api/summary` with no `Remote-Groups` header. Since non-admins were restricted (af94603) that endpoint returns 403, so the task can't reach 200. Add `headers: {Remote-Groups: admins}` or probe `/jobs`. Alternatively, wait on the container's health status, which the image now provides. 4. **`act_runner`:** add buildx to `act-runner-docker-cli` (above). 5. Update homelab's release flow: after `scripts/sync_fabric.sh vX.Y.Z`, also bump the image tag. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Publish multi-arch container image to the Forgejo registry
All checks were successful
Build and Push / test (pull_request) Successful in 24s
Build and Push / build-and-push (pull_request) Has been skipped
d844111f10
Add a root Dockerfile and a build workflow that pushes
git.scruffydude.net/scruffydude/homelab-fabric as a linux/amd64 +
linux/arm64 manifest list (short-sha always, latest on main, v* on tags),
so homelab can pull the ai-fabric image instead of building it on channel66.

`serve` now takes every default from AI_FABRIC_* env vars (flags still
win), and the image sets them to today's deployment values, so it runs
with no extra config given the /data, /repo and /root/.ssh mounts.

tests/smoke_image.sh starts an image against tests/fixtures and checks
health, /api/summary and /api/jobs; CI runs it on the amd64 build before
pushing. ci.yml is folded into build.yml's test job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nimby merged commit 7a5fe53e5b into main 2026-10-09 17:42:32 -04:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
scruffydude/HomelabFabric!1
No description provided.