ci(docker): mirror the all-in-one image to Docker Hub

The aio image (#1042) shipped GHCR-only with a TODO to wire the Docker
Hub mirror once the Hub repo existed. backend, frontend and the ml
sidecar all publish to docker.io/picpeak/*; aio was the only image a
Docker Hub user could not pull.

merge-aio now follows merge-backend/merge-ml verbatim: DOCKERHUB_ENABLED
computed from the repository slug (so forks stay GHCR-only), a gated
Docker Hub login, docker.io/picpeak/aio added to the metadata images
list, and a Docker Hub manifest inspect. Tag scheme is untouched — the
same beta/main/stable/latest/semver tags land in both registries.

The build summary drops the "Docker Hub mirror pending" note and lists
the aio (and ml) Hub images when the mirror is active.
This commit is contained in:
Luca
2026-08-20 07:49:30 +02:00
parent 54b68fe6e8
commit 2df455784c
2 changed files with 34 additions and 8 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
This GitHub Actions workflow automatically builds and pushes Docker images for the backend, the frontend, and the all-in-one image to GitHub Container Registry (ghcr.io).
The **all-in-one image** (`<repo>/aio`, built from `Dockerfile.aio` at the repo root, #1042) bundles the backend and the built frontend into a single container with SQLite as the default engine — one `docker run`, no compose. It follows the same per-arch build → digest-merge → per-version tag scheme as the other two images, is currently GHCR-only (the Docker Hub mirror gets wired later), and every PR additionally runs a `smoke-aio` job that boots the image and asserts the SPA shell, brand-title rendering, immutable asset caching, and the SQLite engine resolution.
The **all-in-one image** (`<repo>/aio`, built from `Dockerfile.aio` at the repo root, #1042) bundles the backend and the built frontend into a single container with SQLite as the default engine — one `docker run`, no compose. It follows the same per-arch build → digest-merge → per-version tag scheme as the other two images, is mirrored to Docker Hub (`docker.io/picpeak/aio`) alongside GHCR on the canonical org repo, and every PR additionally runs a `smoke-aio` job that boots the image and asserts the SPA shell, brand-title rendering, immutable asset caching, and the SQLite engine resolution.
## Features
+33 -7
View File
@@ -780,6 +780,15 @@ jobs:
run: |
repo_lc="${GITHUB_REPOSITORY,,}"
echo "AIO_IMAGE_NAME=${repo_lc}/aio" >> "$GITHUB_ENV"
# Mirror manifests to Docker Hub (picpeak/aio) only on the canonical org
# repo, where the DOCKERHUB_* secrets live. Forks (and any other owner)
# fall back to GHCR-only — the Docker Hub image line and login are gated
# on this flag so their builds keep working unchanged.
if [[ "$GITHUB_REPOSITORY" == "PicPeak/picpeak" ]]; then
echo "DOCKERHUB_ENABLED=true" >> "$GITHUB_ENV"
else
echo "DOCKERHUB_ENABLED=false" >> "$GITHUB_ENV"
fi
- name: Download digest artifacts
uses: actions/download-artifact@v4
@@ -811,16 +820,24 @@ jobs:
echo "is_prerelease=false" >> $GITHUB_OUTPUT
fi
# Same per-version tag scheme as backend/frontend: every Release Please
# version publishes a matching aio image. GHCR-only for now — the Docker
# Hub mirror (docker.io/picpeak/aio) is wired later once the Hub repo
# exists: add the images line + Docker Hub login exactly like
# merge-backend (#1042).
- name: Log in to Docker Hub
if: env.DOCKERHUB_ENABLED == 'true'
uses: docker/login-action@v3
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Same per-version tag scheme as backend/frontend/ml: every Release Please
# version publishes a matching aio image, mirrored to Docker Hub
# (docker.io/picpeak/aio) on the canonical org repo (#1042).
- name: Extract metadata for AIO
id: meta-aio
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.AIO_IMAGE_NAME }}
images: |
${{ env.REGISTRY }}/${{ env.AIO_IMAGE_NAME }}
${{ env.DOCKERHUB_ENABLED == 'true' && 'docker.io/picpeak/aio' || '' }}
labels: |
org.opencontainers.image.title=PicPeak All-in-one
org.opencontainers.image.description=PicPeak backend + frontend in a single container (SQLite default)
@@ -857,6 +874,11 @@ jobs:
run: |
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.AIO_IMAGE_NAME }}:${{ steps.meta-aio.outputs.version }}
- name: Inspect manifest (Docker Hub)
if: env.DOCKERHUB_ENABLED == 'true'
run: |
docker buildx imagetools inspect docker.io/picpeak/aio:${{ steps.meta-aio.outputs.version }}
# Boot-level verification of the AIO image on every PR: build for the
# runner's arch, run it with no DB env (SQLite default), and assert the
# things nginx used to guarantee — SPA shell with the brand title rendered,
@@ -1388,10 +1410,14 @@ jobs:
if [[ "${{ needs.merge-ml.result }}" == "success" ]]; then
echo "- ML sidecar (optional): \`${{ env.REGISTRY }}/${{ env.ML_IMAGE_NAME }}\`" >> $GITHUB_STEP_SUMMARY
fi
echo "- All-in-one: \`${{ env.REGISTRY }}/${{ env.AIO_IMAGE_NAME }}\` (GHCR only — Docker Hub mirror pending)" >> $GITHUB_STEP_SUMMARY
echo "- All-in-one: \`${{ env.REGISTRY }}/${{ env.AIO_IMAGE_NAME }}\`" >> $GITHUB_STEP_SUMMARY
if [[ "$DOCKERHUB_ENABLED" == "true" ]]; then
echo "- Backend (Docker Hub): \`docker.io/picpeak/backend\`" >> $GITHUB_STEP_SUMMARY
echo "- Frontend (Docker Hub): \`docker.io/picpeak/frontend\`" >> $GITHUB_STEP_SUMMARY
echo "- All-in-one (Docker Hub): \`docker.io/picpeak/aio\`" >> $GITHUB_STEP_SUMMARY
if [[ "${{ needs.merge-ml.result }}" == "success" ]]; then
echo "- ML sidecar (Docker Hub): \`docker.io/picpeak/ml\`" >> $GITHUB_STEP_SUMMARY
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY