Repository navigation
Conversation
The New Relic tarball ships newrelic-daemon.x64 and newrelic-daemon.aarch64; the hardcoded x64 copy made an arm64 build ship an amd64 binary.
mailhog/mailhog:v1.0.1 is an amd64-only image from 2020 on golang:1.14 alpine 3.12, and upstream is unmaintained. Build the same v1.0.1 tag from its vendored sources in a Go builder stage and ship it on alpine 3.24.3 with the same user (mailhog, uid 1000), workdir, entrypoint and ports. The Go toolchain and its GOPATH/GOLANG_VERSION env are no longer in the image, which shrinks it from 572 MB to 29 MB.
One script for CI and local runs: it checks the image platform and runs each image's main checks (php modules, gd formats, composer, wp-cli, php-fpm -t and New Relic; nginx -t; docker-gen and the proxy template; redis PING; mariadb SELECT VERSION(); postfix status; ofelia validate; a MailHog SMTP round trip; the New Relic daemon).
Each image is built and smoke-tested on a native runner per arch (ubuntu-24.04 and ubuntu-24.04-arm). On v* tags in EasyEngine/dockerfiles the per-arch images are pushed by digest and a merge job creates the multi-arch easyengine/<img>:<tag>, then checks that it lists both platforms. The merge runs per image, so one failed image doesn't block the others, and an image that is missing an arch is not published. Pull requests build and test both arches without pushing. Other runs (workflow_dispatch, tags on forks) are dry runs: the per-arch OCI layouts go through artifacts into a local registry, and the same manifest step runs against it. No layer cache, so tag builds still fetch current packages.
The artifact pattern digests-nginx-* also matched digests-nginx-proxy-*, so the nginx merge saw four digests and refused to publish. Match the two arches explicitly.
A binary for the wrong arch can still run through QEMU/binfmt, so version checks alone don't prove an arm64 image ships arm64 binaries.
Keeps base-image pulls authenticated on publish runs, as in the old workflow.
A Dockerfile can mount it (RUN --mount=type=secret,id=github_token) to authenticate GitHub downloads instead of hitting the anonymous API rate limit. BuildKit secrets never land in a layer or the cache key, so the publish build still reuses the tested layers, and builds that don't mount it are unaffected.
There was a problem hiding this comment.
🟢 Approval recommended
No unresolved findings remain, and the reported verification runs passed builds, smoke tests, and manifest checks for both architectures.
No open issues 🎉
What changed in this PR
Adds native linux/amd64 and linux/arm64 builds for all 13 images in the publishing matrix, enabling future releases to support arm64 hosts while preserving tag-only Docker Hub publishing.
Changes:
- Builds and smoke-tests both architectures, then combines their digests into multi-architecture tags; adds a local-registry dry-run mode.
- Adds platform, binary architecture, and per-image runtime smoke checks.
- Builds MailHog from source and selects the correct New Relic daemon binary for each architecture.
| File | Description |
|---|---|
newrelic-daemon/Dockerfile |
Selects the daemon binary for amd64 or arm64. |
mailhog/Dockerfile |
Replaces the amd64-only base with a multi-stage source build. |
.github/workflows/test_and_publish.yml |
Adds dual-architecture builds, smoke tests, digest merging, and dry runs. |
.github/scripts/smoke-test.sh |
Checks image platforms, binary architectures, and service functionality. |
Review effort: Balanced
php5.6, php7.0, php7.2, php7.3, php7.4, php8.0 and mariadb (10.11) are no longer in the build matrix, so they stay amd64-only. legacy-arm64.yml (workflow_dispatch only) rebuilds each published amd64 image for arm64 from a recipe and publishes a new tag whose index holds the untouched amd64 image, by digest, plus the new arm64 one. Each recipe names the git ref that matches the published image's history and layers (for php5.6-7.3 that is not the v4.7.4 tree), plus a small patch: a Debian snapshot of the amd64 build date for the frozen bases, PECL versions pinned to the ones in the amd64 image, and for mariadb the 10.11.14 base index that v4.9.1 was really built from. Before anything is pushed, the arm64 image must match the published amd64 one: image config, PHP version, php -m, extension versions, ini values, php-fpm -t, every file under /usr/local/etc (MariaDB: SELECT VERSION(), server variables, /etc/mysql), and its key binaries must be arm64 ELF files. The new tag is refused if it already exists. Without publish it is a dry run into a runner-local registry. New Relic ships no aarch64 agent for PHP 7.x, so php7.0-7.4 refuse to build until their recipe says "newrelic": "skip".
…ch release files by hash Forks don't carry the upstream v4.13.0 and v4.9.1 tags, so recipes name the commits and the script fetches a missing one from EasyEngine/dockerfiles. Stretch's archive keys have expired, so apt rejects the snapshot; php5.6 and php7.0 now pin the snapshot's release files to the copies verified against the stretch keys as of the build date, and apt checks every index and package against them.
…ison wsrep_node_name defaults to the hostname, which differs per container.
PHP 8.0 has no JIT on AArch64 (it came in 8.1), so its opcache.jit* settings don't exist there. The recipe names them; the check drops them from the amd64 side and fails if arm64 has any. JIT is off on amd64 too (opcache.jit_buffer_size=0).
jq -e prints null for a missing field, which turned the php8.0 ini allowance on for every image.
… runs per image and tag The stretch recipes checked separately downloaded copies of the release files, but apt fetched its own. They now pin the hashes of the files apt stores and check them right after apt-get update, before anything is upgraded or installed; apt then checks every index and package against those files. Build jobs for the same image and tag now share a non-cancelling concurrency group, so a second run waits and then refuses the tag the first one created.
New Relic publishes no aarch64 agent for PHP 7.x, so the arm64 variants of php7.0, php7.2, php7.3 and php7.4 ship without it. newrelic.ini stays, without its extension line, so the entrypoint still finds it; the parity check allows exactly the New Relic lines to differ.
Core stays on MariaDB 10.11, so the tag run no longer builds mariadb/Dockerfile (12.3). The one-time legacy-arm64 workflow publishes the multi-arch 10.11.14 image instead.
The legacy PHP images and MariaDB 10.11.14 all get their amd64 + arm64 index under v4.14.0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Builds, smoke-tests and publishes every image in the build matrix for
linux/amd64andlinux/arm64, so EasyEngine can run on arm64 Linux hosts and Apple Silicon. This PR edits.github/workflows/test_and_publish.yml(rewritten) and adds.github/scripts/smoke-test.sh. It also adds a separate one-time workflow for arm64 variants of the images outside the matrix (.github/workflows/legacy-arm64.yml,.github/legacy-arm64/,.github/scripts/legacy-arm64.sh; see the section below).What changes
Workflow (
test_and_publish.yml):buildjob, matriximage(12) ×arch(amd64, arm64) = 24 jobs,fail-fast: false. amd64 runs onubuntu-24.04, arm64 on the nativeubuntu-24.04-armrunner (free for public repos). No QEMU: emulated PHP builds are about 17× slower, and a wrong-arch binary still "works" under binfmt.load: true, then runs.github/scripts/smoke-test.shagainst it.digests-<img>-<arch>artifact (retention 1 day).mergejob per image (needs: build,if: !cancelled(), so one failed image doesn't block the others) downloads both digests, refuses to publish unless both arches are present, createseasyengine/<img>:<tag>withdocker buildx imagetools create, and checks that the index lists exactlylinux/amd64,linux/arm64. An image that fails on either arch is not published at all, never half-published.v*tag pushed toEasyEngine/dockerfiles: Docker Hub login (sameDOCKERHUB_USERNAME/DOCKERHUB_TOKENsecrets) before the first build, push by digest, multi-arch tag. Publishing still happens only onv*tags, as before. Nolatesttag, no build ondeveloppushes, every matrix image pushed on each tag, as before.pull_request: build and smoke-test both arches; no push, no merge job.workflow_dispatch, which is new, or tags on forks): a secret-free dry run. Each arch's OCI layout goes through artifacts into a throwawayregistry:3on the runner, and the same manifest + verify steps run againstlocalhost:5000. After merging,gh workflow run test_and_publish.yml --ref developexercises the whole publish path before tagging.GITHUB_TOKENas the optional BuildKit secretgithub_token. Secrets don't land in layers or in the cache key; builds that don't mount it ignore it.apt-get upgrade, so a cache would ship stale layers on tags.Smoke test (
.github/scripts/smoke-test.sh <image> <ref> <arch>, also usable locally): checks the image platform, that each image's key binaries are ELF files for the target arch (e_machinex86-64 / AArch64), then per image: php modules (incl. imagick, redis, memcached, imap, gmagick and New Relic where built), gd formats, composer, wp-cli,php-fpm -t;nginx -t; docker-gen, forego and the proxy template; redisPINGwith EE's config; mariadbSELECT VERSION(); postfix start + status;ofelia validate; a MailHog SMTP round trip with EE's-invite-jim=false; the New Relic daemon staying up.Per-image fixes:
newrelic-daemon: copied the hardcodednewrelic-daemon.x64, so an arm64 build would ship an amd64 binary. It now picksx64/aarch64fromapk --print-archand fails the build on any other arch.mailhog:mailhog/mailhog:v1.0.1is amd64-only (a single 2020 manifest) and upstream is unmaintained. The image now builds the samev1.0.1tag from its vendored sources (golang:1.27.3-alpine3.24, GOPATH mode,CGO_ENABLED=0) and ships it onalpine:3.24.3with the same user (mailhog, uid 1000), workdir, entrypoint, ports and flags. Behaviour is unchanged (same UI and API; EE's-invite-jim=falsestill applies). The Go toolchain is no longer in the image, which shrinks it from 572 MB to about 29 MB. Dependabot's existing/mailhogentry now tracks thegolangandalpinebases.agent/aarch64itself.Which images get arm64: all 12 in the matrix (php8.1–8.5, nginx, nginx-proxy, postfix, redis, cron, mailhog, newrelic-daemon). mariadb leaves the tag-triggered matrix: core stays on MariaDB 10.11, so the tag run no longer builds
mariadb/Dockerfile(12.3); the multi-arch 10.11.14 image comes from the one-time workflow below. php5.6–8.0 also stay out of the matrix under the EOL-base policy and get arm64 variants once through the same workflow.easyengine/php(stable) stays amd64-only. Existing tags are never re-pushed, so only the next dockerfiles tag is multi-arch.Verification
Dry-run publish on a fork branch (the trigger widened to that branch is the only workflow difference from this PR):
39996556827 (current head, mariadb out of the matrix): all 36 jobs green (24 build + smoke, 12 merges), every merged manifest
linux/amd64,linux/arm64. The runs below still had mariadb (12.3) in the matrix.39961111371: all 39 jobs green (26 build + smoke, 13 merges). 88 ELF checks passed; every merged manifest verified
linux/amd64,linux/arm64.amd64 equivalence with the published
v4.13.3images (develop's tree equals thev4.13.3tag), comparing image config (User,Entrypoint,Cmd,WorkingDir,Env,ExposedPorts,Volumes,Labels,StopSignal), the full package list (dpkg-query/apk info) and, for php,php -mand the PHP/composer/wp-cli/New Relic versions:tzdata2026c → 2026e on 8.1 (itsapt-get upgrade)php -midentical; PHP 8.1.34/8.2.35/8.3.36/8.4.27/8.5.12 (same builds), Composer 2.10.3, WP-CLI 2.12.0, New Relic 12.12.0.41 (not 8.5)tzdata2026c → 2026e onlybrotli-libs1.2.0 → 1.2.1 onlyopenrestyre-pushed its image on 2026-10-22)ca-certificates,perl-baseDebian point updates onlyPATH/GOPATH/GOLANG_VERSIONenv is gone; user, entrypoint, workdir, ports and labels identicalThe non-mailhog differences are upstream package updates published after the v4.13.3 tag run, not changes from this PR.
39961114837: the same branch merged with every open PR (build(php): install extensions with PIE and fail the build on missing modules #367–fix(php): drop deprecated session ID ini settings from 8.4 and 8.5 #372, build(php): install extensions with PIE and fail the build on missing modules #367 resolved as below): all 39 jobs green, 88 ELF checks, 13/13 manifests
linux/amd64,linux/arm64, so the PIE builds, postfix 3.11.7 and the other refreshed images also build and pass on arm64.CI time
PR runs go from 13 amd64 jobs to 24 jobs (12 per arch, in parallel on native runners). Tag runs go from 13 jobs to 36 (24 builds + 12 short merge jobs). Wall time: about 8 minutes (8 m 15 s for 39961111371; arm64 PHP builds 5–6.5 min, the same as amd64) for a full dry run, against about 6 minutes for the old amd64-only tag run; PHP builds take about the same on arm64 as on amd64. Runner minutes roughly double, which is free on public repos.
Conflicts with open PRs
test_and_publish.ymlonly: it addssecrets: github_token=${{ secrets.GITHUB_TOKEN }}to the two PHP build steps of the old workflow. This PR already passes that secret to both build steps of every image, so whichever merges second resolves the conflict by keeping this PR's version oftest_and_publish.ymlunchanged (git checkout --ours/--theirsto the multi-arch side, no hand-editing). The PIE Dockerfiles then build on both arches (verified in 39961114837).FROMline, this PR only theRUNbody.One-time arm64 builds of the legacy images
php5.6, php7.0, php7.2, php7.3, php7.4, php8.0 and mariadb (10.11, the version core pins) aren't in the build matrix, so the tag run doesn't make them multi-arch. A separate workflow,
.github/workflows/legacy-arm64.yml(workflow_dispatchonly), gives each of them an arm64 variant once. It never touches the amd64 images: it builds only arm64, then publishes a new tag (v4.14.0) whose index holds the published amd64 image by digest (unchanged) plus the new arm64 image. Existing tags aren't overwritten, and the legacy images don't join the regular matrix.easyengine/php(stable) isn't included: no site uses it. There's no php7.1 recipe either: it isn't supported by EasyEngine and has no Docker Hub repository.Recipes (
.github/legacy-arm64/recipes.jsonplus one<image>.patcheach): each recipe names the published amd64 digest, the commit that matches that image, and a small patch.v4.7.4php5.6–7.3 images weren't built from thev4.7.4tree (they predate the tmux/usermod change, and 7.2/7.3 also lack WebP). Their layers and history match681d01a(5.6),7b74e92(7.2) and2807ccb(7.3). php7.0 matchesa528d29plus anapt-get upgrade -yand theopensslpackage that its history shows but no commit has. php7.4 and php8.0 use thev4.13.0tree.apt-get update, before anything is upgraded or installed; apt then checks every index and package against those files. PECL versions are pinned to the ones in the amd64 images, and php8.0's New Relic agent to 12.11.0.40.docker-php-ext-enableline that would also enable imagick then fails), so several extensions are compiled but not enabled. The arm64 builds reproduce that exactly instead of diverging between arches.easyengine/mariadb:v4.9.1is MariaDB 10.11.14 on Ubuntu 22.04, although its tag's Dockerfile saysFROM mariadb:11.5. Its eight base layers are the amd64 image ofmariadb:10.11.14@sha256:e4ad7c5a…(the index of 2025-09-02 recorded in docker-library/repo-info), which is still pullable and has an arm64 image from the same build. The recipe buildsFROMthat index with the tag'see.cnfandmy.cnf, which are byte-identical to the published layers. No version change.Checks before anything is pushed (arm64 build vs. the published amd64 image, collected on native runners): image config, PHP version,
php -m, every extension version, every ini value,php-fpm -t, and the checksum of every file under/usr/local/etcmust be identical. For MariaDB:SELECT VERSION(),SHOW GLOBAL VARIABLES(minus host-specific ones) and every file under/etc/mysql. php, php-fpm, every extension.so, and mariadbd/mariadb/gosu must be AArch64 ELF files. Package lists and Composer/WP-CLI versions are logged for information. Then the new tag is refused if it exists, the index is created, and the job checks that it lists exactlylinux/amd64,linux/arm64with the published amd64 digest and the tested arm64 digest. Finally it pulls the tag on the arm64 runner and repeats the checks against the pulled image.Known, documented arm64 differences:
"newrelic": "skip"), while their amd64 images keep it. The agent step is dropped,newrelic.iniis kept without itsextension=line so the entrypoint's edit still works, and the check allows exactly the New Relic lines to differ. php5.6 never had New Relic; php8.0 has it on both arches (agent 12.11.0.40). A recipe set to"unavailable"stops with an explicit message instead.opcache.jit*settings don't exist there (JIT is off on amd64:opcache.jit_buffer_size=0).libquadmath0, binNMU rebuilds).Verification (dry runs on a fork branch; the only temporary changes were a push trigger and default inputs):
39996556771 (current head, recipes exactly as committed,
tag=v4.14.0): all 15 jobs green. All seven arm64 images match their published amd64 images on every check; the only allowed differences are New Relic on php7.0–7.4 and php8.0's absentopcache.jit*. ELF checks: 10–29 per PHP image and 4 for mariadb.v4.14.0is free on Docker Hub for all seven. In each combined index the amd64 digest equals the published one: php5.60d02a5f8, php7.0dc42a302, php7.2bd388833, php7.3e85e80a6, php7.484463e41, php8.0b7da1852, mariadb60774518. MariaDB returned10.11.14-MariaDB-ubu2204on both arches, with identical server variables (minus hostname-derived ones),/etc/mysqland package list._ReleasefilesOKright afterapt-get update, every check identical, amd64 digests unchanged.Earlier runs:
opcache.jit*and php7.x's New Relic are the only allowed differences). ELF checks: 10–29 binaries per PHP image, 4 for mariadb. MariaDB returned10.11.14-MariaDB-ubu2204on both arches, with identical server variables (minus hostname-derived ones),/etc/mysqland package list. In each combined index the amd64 digest equals the published one: php5.60d02a5f8, php7.0dc42a302, php7.2bd388833, php7.3e85e80a6, php7.484463e41, php8.0b7da1852, mariadb60774518.libquadmath0, binNMU+b1rebuilds). Composer/WP-CLI differ only on the images built before 2026 (e.g. php5.6: Composer 2.2.21 → 2.2.30, WP-CLI 2.7.1 → 2.12.0).images=mariadb,tag=v4.9.1: the build and checks passed, then "Refuse an existing tag" failed witheasyengine/mariadb:v4.9.1 already existsbefore anything was pushed.Runbook (once, after this PR is merged, from
develop):imagesdefaults to all seven andtagtov4.14.0. A subset can be re-run (-f images=mariadb); an image whose tag already exists is refused. Build jobs for the same image and tag share a non-cancelling concurrency group, so a second run waits and then refuses the tag the first one created. The job summary lists the amd64 and arm64 digests per image. Core then moves the pins inimg-versions.jsontov4.14.0.After merging
Tag as usual; the tag run publishes two-platform indexes. arm64 hosts then need core to pin the new tags in
img-versions.json.