From 75d142df25ea4e6a790173732d0ad4aadbc2a25e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 26 Apr 2026 23:17:48 +0200 Subject: [PATCH] feat(ci): publish multi-arch Docker image to GHCR (#88) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #88's biggest blocker was that docs and README pointed at ghcr.io/mbombeck/healthlog:latest, but no workflow ever pushed an image — every self-hoster trying to follow the docs hit "manifest unknown". This adds .github/workflows/docker-publish.yml. The workflow: - Triggers on push to main, on v* tags, on PRs (build only, no push), and via workflow_dispatch - Builds linux/amd64 + linux/arm64 multi-arch images via buildx - Tags with `latest` (main only), semver (`1.2.0`, `1.2`), branch name, PR number, and short SHA — using docker/metadata-action defaults - Attaches SLSA build provenance and an SBOM to every image - Caches build layers in GitHub Actions cache for fast iterations - Uses concurrency group + cancel-in-progress so superseded runs stop The bundled docker-compose.yml now references `ghcr.io/mbombeck/healthlog:latest` as the primary `image:` and keeps `build:` as a fallback so contributors can still `docker compose up --build` against their working tree. SECURITY.md gains a Supply Chain section explaining where images come from, how to verify provenance and SBOMs, and how to pin a specific version. README's Quick Start updated to reflect that no build is needed for self-hosters. The first run of this workflow on main will publish the image; until then `docker compose up` will fall back to the `build:` block. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/docker-publish.yml | 78 ++++++++++++++++++++++++++++ README.md | 2 +- SECURITY.md | 16 +++++- docker-compose.yml | 7 +++ 4 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/docker-publish.yml diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 000000000..13b59fca5 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,78 @@ +name: Build & Publish Docker Image + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + branches: [main] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +# Cancel in-progress runs when a new commit is pushed to the same ref. +concurrency: + group: docker-publish-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-push: + name: Build & push multi-arch image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU (multi-arch) + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Skip login on PRs from forks — they don't have package:write. + # Image is built but not pushed in that case. + - name: Log in to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compute image tags & labels + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + type=sha,prefix=sha-,format=short + labels: | + org.opencontainers.image.title=HealthLog + org.opencontainers.image.description=Self-hosted health-tracking PWA + org.opencontainers.image.licenses=AGPL-3.0-only + org.opencontainers.image.source=https://github.com/${{ github.repository }} + + - name: Build & push image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: true + sbom: true diff --git a/README.md b/README.md index 250c930a9..b02ff8ad8 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Most health apps lock your data behind proprietary clouds, push subscriptions, a ## Quick Start -Plan ~5 minutes for a working install. The bundled `docker-compose.yml` builds the app from source — pre-built images on GHCR are coming in a separate release. +Plan ~5 minutes for a working install. The bundled `docker-compose.yml` pulls a pre-built multi-arch image (`linux/amd64` + `linux/arm64`) from [GitHub Container Registry](https://github.com/MBombeck/HealthLog/pkgs/container/healthlog); no build step required for self-hosters. Contributors who want to test local changes can `docker compose up --build`. ```bash git clone https://github.com/MBombeck/HealthLog.git diff --git a/SECURITY.md b/SECURITY.md index 8e24b8d2c..fadaf9b98 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,7 +5,7 @@ Only the latest release on the `main` branch is actively supported with security updates. | Version | Supported | -|---------|-----------| +| ------- | --------- | | Latest | Yes | | Older | No | @@ -59,3 +59,17 @@ HealthLog is designed with security as a core principle: - **Proxy-level route protection** via `proxy.ts` (session cookie validation on all non-public paths) For more details, see the [security documentation](https://docs.healthlog.dev/security/overview/). + +## Supply Chain + +HealthLog Docker images are built and published from this repository's CI: + +- **Source**: [`.github/workflows/docker-publish.yml`](.github/workflows/docker-publish.yml) — runs on every push to `main` and every `v*` tag +- **Registry**: [`ghcr.io/mbombeck/healthlog`](https://github.com/MBombeck/HealthLog/pkgs/container/healthlog) +- **Architectures**: `linux/amd64` and `linux/arm64` +- **Provenance attestation**: each image carries a [SLSA build provenance](https://slsa.dev/spec/v1.0/provenance) statement linking it back to the GitHub Actions run, the commit SHA, and the workflow definition +- **SBOM**: each image includes a [Software Bill of Materials](https://docs.docker.com/build/metadata/attestations/sbom/) you can inspect with `docker buildx imagetools inspect ghcr.io/mbombeck/healthlog:latest --format '{{ json .SBOM }}'` + +To pin to a specific version in production, replace `:latest` with the released tag in your `docker-compose.yml`, e.g. `ghcr.io/mbombeck/healthlog:1.2.0`. Pinning is recommended for self-hosters who want explicit control over upgrades. + +If you discover a tampered or unexpected image, please report it as a vulnerability via the email above. diff --git a/docker-compose.yml b/docker-compose.yml index c990c15bc..f5405eae6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,12 @@ services: app: + # Pre-built multi-arch images are published to GHCR by + # .github/workflows/docker-publish.yml on every push to main and on + # tagged releases. Pin to a specific version in production: + # e.g. ghcr.io/mbombeck/healthlog:1.2.0 + image: ghcr.io/mbombeck/healthlog:latest + # Fallback build context: if the image is not yet in the registry, + # or you want to test local changes, run `docker compose up --build`. build: context: . dockerfile: Dockerfile