ci: publish control-plane image for Swarm deploys

Swarm ignores docker-compose.yaml's build: key, so docker stack deploy
previously needed an operator to build and tag the control-plane image
by hand. CI now publishes it to this instance's container registry on
every push to master (or a tag), and docker-compose.yaml's image: key
(overridable via GATEWAY_CONTROL_PLANE_IMAGE) lets Swarm pull it directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0142mP6EqCkQKmR1xCp7zfmZ
This commit is contained in:
Robert Syrnicki 2026-08-26 12:20:16 +02:00
parent 73c96cab56
commit 8eebae8e1e
4 changed files with 89 additions and 5 deletions

View file

@ -47,3 +47,10 @@ GATEWAY_ADMIN_TOKEN=
# DNS provider Traefik's DNS-01 challenge should use for the wildcard cert.
# See traefik/static/traefik.shared.yml.
GATEWAY_BASE_DOMAIN=boxes.example.com
# Optional: override the control-plane image `docker stack deploy` pulls
# (Swarm ignores docker-compose.yaml's `build:` key — see README's Swarm
# caveats). Defaults to the `latest` tag CI publishes on every push to
# master; pin to a specific commit-sha tag for a reproducible deploy.
# Not read by plain `docker compose up`, which always builds locally.
GATEWAY_CONTROL_PLANE_IMAGE=

View file

@ -0,0 +1,67 @@
name: Build control-plane image
# Publishes control_plane as a versioned image to this Forgejo instance's
# own container registry, so `docker stack deploy` (which never reads
# `build:`, see README's Swarm caveats) doesn't need an operator to build
# and tag it by hand first.
#
# Self-hosted runner, plain `docker build`/`docker push` rather than the
# docker/build-push-action marketplace action: unconfirmed whether that
# action is mirrored on this instance's action runner (furtka's own
# ci.yml hit exactly this gap with lycheeverse/lychee-action and had to
# fall back to a fully-qualified github.com URL), and raw docker commands
# sidestep the question entirely — same reasoning as release.yml/
# build-iso.yml using plain shell for their heavy lifting.
on:
push:
branches: [master]
tags: ['**']
paths:
- 'control_plane/**'
- '.forgejo/workflows/build-image.yml'
workflow_dispatch:
concurrency:
group: build-image-${{ github.ref }}
cancel-in-progress: true
jobs:
build-image:
runs-on: self-hosted
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Log in to this instance's container registry
# GITHUB_TOKEN here is Forgejo's own auto-issued Actions token,
# scoped to this repo with package read/write — not a GitHub
# token. Standard Forgejo/Gitea Actions pattern.
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login "${{ vars.FORGEJO_HOST || 'forgejo.sourcegate.online' }}" \
-u "${{ github.actor }}" --password-stdin
- name: Build image
run: |
docker build -t control-plane:build ./control_plane
- name: Tag and push
env:
REGISTRY: ${{ vars.FORGEJO_HOST || 'forgejo.sourcegate.online' }}
IMAGE: furtka-gateway-control-plane
run: |
set -e
sha_tag="${GITHUB_SHA::12}"
docker tag control-plane:build "$REGISTRY/${{ github.repository_owner }}/$IMAGE:$sha_tag"
docker push "$REGISTRY/${{ github.repository_owner }}/$IMAGE:$sha_tag"
# `latest` tracks master; a version-tag push (e.g. from a future
# CalVer release process, see furtka/RELEASING.md for the
# pattern this would mirror) additionally publishes under that
# tag's own name.
if [ "${{ github.ref_type }}" = "branch" ]; then
docker tag control-plane:build "$REGISTRY/${{ github.repository_owner }}/$IMAGE:latest"
docker push "$REGISTRY/${{ github.repository_owner }}/$IMAGE:latest"
else
docker tag control-plane:build "$REGISTRY/${{ github.repository_owner }}/$IMAGE:${{ github.ref_name }}"
docker push "$REGISTRY/${{ github.repository_owner }}/$IMAGE:${{ github.ref_name }}"
fi

View file

@ -69,11 +69,13 @@ plan, not yet implemented — see the plan doc for the phased delivery.
If deploying via `docker stack deploy` instead of `docker compose up`:
- `docker-compose.yaml`'s `build: ./control_plane` doesn't work under
Swarm at all — it only ever schedules pre-built, tagged images. Build
and tag the image yourself first (`docker build -t
furtka-gateway-control-plane:local ./control_plane`) and swap `build:`
for `image: furtka-gateway-control-plane:local` until this is packaged
as a versioned image in CI (the planned long-term fix).
Swarm at all — it only ever schedules pre-built, tagged images. Swarm
falls back to the `image:` key on the same service instead, which
`.forgejo/workflows/build-image.yml` keeps published to this instance's
container registry on every push to `master` (tagged `latest` plus the
short commit sha) — no manual build/tag step needed. Set
`GATEWAY_CONTROL_PLANE_IMAGE` in `.env` first if you want a specific
commit-sha tag rather than whatever `latest` currently points to.
- `docker stack deploy` does not read a `.env` file next to the compose
file the way `docker compose up` does — `set -a; source .env; set +a`
first, or the `${VAR}` substitutions come out empty.

View file

@ -72,6 +72,14 @@ services:
control-plane:
build: ./control_plane
# CI (.forgejo/workflows/build-image.yml) publishes this same build to
# the registry on every push to master, tagged `latest` plus the short
# commit sha. `docker compose up` builds fresh from `build:` above and
# tags the result as this name, ignoring this being a registry ref;
# `docker stack deploy` (Swarm ignores `build:` entirely, see the
# Swarm caveats in README.md) pulls it instead — no manual
# `docker build -t ... ./control_plane` step needed there anymore.
image: ${GATEWAY_CONTROL_PLANE_IMAGE:-forgejo.sourcegate.online/robert/furtka-gateway-control-plane:latest}
container_name: furtka-gateway-control-plane
environment:
- GATEWAY_MODE=${GATEWAY_MODE:-single}