ci: build docker images on native amd64 + arm64 runners instead of QEMU emulation
- Dominant language
- Python
- Stars
- 2
- Forks
- 0
- Avg merge
- 15h 59m
- Merged PRs (30d)
- 9
Description
Follow-up to #71. Every place that builds `docker/Dockerfile` today cross-builds `linux/arm64` under QEMU on an amd64 runner, which makes it the slowest thing in the repo:
- `ci.yml` → `docker-image` (check name "Docker image (amd64 + arm64)"), `timeout-minutes: 45`. Uses `docker/setup-qemu-action`, then runs `pytest tests/image/` with `REMO_RUN_IMAGE_TESTS=1`. This is now a required status check on `main` (see #71), so it gates every PR merge.
- `release.yml` → `publish-image`, `timeout-minutes: 60`. `docker/build-push-action` with `platforms: linux/amd64,linux/arm64`, pushing the manifest to `ghcr.io/get2knowio/remo-web`.
Emulated `npm ci` + `pip install` is roughly an order of magnitude slower than native. GitHub now offers hosted arm64 Linux runners (`ubuntu-24.04-arm` / `ubuntu-22.04-arm`) that are free for public repos, so we can build each architecture natively and drop QEMU entirely.
## Proposed changes
### 1. `ci.yml` — fan the `docker-image` job out across two runners
One job per arch, each building only its own platform:
```yaml
docker-image:
name: Docker image (${{ matrix.arch }})
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
timeout-minutes: 20
```
- Drop `docker/setup-qemu-action`.
- `tests/image/` needs to build and assert only the arch it is running on. Coverage of the arch-selection branch (FR-027/FR-042) is preserved because each leg exercises a different branch natively instead of one runner doing both.
- The 45-minute timeout can come down a lot; pick the real value after the first green run.
### 2. `release.yml` — split `publish-image` into build-by-digest + manifest merge
The standard buildx pattern: each runner builds and pushes by digest, then a final job stitches the multi-arch manifest.
```yaml
build-image:
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
# build-push-action with:
# outputs: type=image,name=ghcr.io//remo-web,push-by-digest=true,name-canonical=true,push=true
# then upload the resulting digest as an artifact
merge-image:
needs: [build-image]
runs-on: ubuntu-latest
# download digests, run docker/metadata-action for tags,
# then: docker buildx imagetools create $TAGS
```
Tag derivation (`type=semver` plus the conditional `latest`) moves to the merge job, so published tags stay identical from a consumer's point of view. `packages: write` stays on the jobs that push.
### 3. Required-status-check bookkeeping
The `main` ruleset currently requires a check literally named `Docker image (amd64 + arm64)`. Renaming to per-arch job names means the ruleset must be updated to require `Docker image (amd64)` and `Docker image (arm64)` instead — otherwise the old name never reports and PRs sit unmergeable forever.
## Risks / notes
- Hosted arm64 runners are free for public repos, but the image is newer than the amd64 one. If an action or apt package misbehaves there, the fallback is native amd64 in CI plus emulated arm64 only at release time.
- Job count doubles, but the two legs run in parallel so wall-clock on the PR gate should drop substantially.
- Build cache is per-arch: include the arch in the `cache-from` / `cache-to` scope to avoid cross-arch cache thrash.
Contributor guide
Research direction
Start with the docker-image job in ci.yml and the publish-image job in release.yml, then inspect tests/image/ and the main ruleset's required status checks. Run the existing image tests before changing the workflows. Done means both architectures build natively in CI, release digests merge into the same tags, and both per-arch checks are required.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, github-actions
- Domain
- build-system, ci-cd, devops
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 48/100