Ever since I got an M2 Mac, I noticed more and more warnings around the images I wanted to use to not be native to my platform. Well, I was aware that Docker images and their content were platform/architecture-specific, but I’ve never really “felt” the effects of that before except when I played around with Docker on Raspberry PIs 🤦♂️ So now that I have both an M2 Mac and an old Intel-based one I started to look into options for how to bundle my applications up for both target systems.
As far as I can tell, there are two options:
myapp:latest-amd64
and myapp:latest-arm64
The first approach is not all that user-friendly as users now have to be aware of what platform they are on. It’s just easier for the user to just pull myapp:latest
and let the Docker runtime handle the rest.
I was curious how some of the tools I regularly use solved this and so I took a look at goreleaser. If you look at the goreleaser/goreleaser:v1.20.0
entry on Docker Hub, you’ll notice that it supports two platforms: linux/amd64
and linux/arm64
. Both of these have image digests listed so there seems to be a completely standalone image also available for each of them:
goreleaser/goreleaser:v1.20.0-arm64
goreleaser/goreleaser:v1.20.0-amd64
These two images are then somehow bundled into one entry for easier consumption. To find out more, I used skopeo inspect --raw docker://goreleaser/goreleaser:v1.20.0
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 2632,
"digest": "sha256:b2c31539d194880b293399c7ac98bc032cb9bbb098c512b0abaa55c8c1541d34",
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 2632,
"digest": "sha256:8bbc939928d0c06c05208004c0f013957ff4d7669f25b5b850f63524f8c67e99",
"platform": {
"architecture": "arm64",
"os": "linux"
}
}
]
}
This entry is actually a list of so-called manifests (one for each of the platforms). The runtime will then pick the right manifest for the platform you’re on (linux/arm64 for M1/2 Macs, linux/amd64 for Intel in my case).
So, how do you now create such a multi-platform image? The Docker manual has a detailed guide with multiple options but I wanted to create a minimal example using this Dockerfile
:
FROM alpine:3.18
ARG TARGETPLATFORM
ARG BUILDPLATFORM
RUN echo "Building on $BUILDPLATFORM for $TARGETPLATFORM" > /log
In this example, I want to have an image on DockerHub with the name zerok/multiarch-test:latest
that I can use on linux/amd64 and linux/arm64.
To make platform specific images, I’ll use buildx:
docker buildx build --platform linux/amd64 \
--tag zerok/multiarch-test:latest-amd64 \
--output=type=docker,dest=latest-amd64.tar .
docker buildx build --platform linux/arm64 \
--tag zerok/multiarch-test:latest-arm64 \
--output=type=docker,dest=latest-arm64.tar .
This will write the generated images to separate .tar
files. For generating the manifest list I’ll need to have these also available within the “normal” Docker engine:
docker load --input latest-arm64.tar
docker load --input latest-amd64.tar
Finally, with those images in place, I’ll create a single manifest file and push that to DockerHub:
docker manifest create \
--insecure zerok/multiarch-test:latest \
--amend zerok/multiarch-test:latest-amd64 \
--amend zerok/multiarch-test:latest-arm64
docker manifest push zerok/multiarch-test:latest
Actually, all of these steps can also be combined into a single command which is is handy if you don’t need to process the images before pushing them to the registry:
docker buildx build \
--platform linux/amd64 \
--platform linux/arm64 \
--tag zerok/multiarch-test:latest \
--push .
Thanks to this little experiment/investigation I found a lot of useful resources around manifests and related topics:
Since we had something to celebrate last week, we wanted to also celebrate it with a fancy dinner. During the last couple of years, a few really nice vegetarian/vegan restaurants have opened in Graz and so we decided to try one that offered a bit of extra fanciness: “Die Speis am Lendhafen”, a tiny but very modern restaurant located at Mariahilferplatz. It only has about five indoor tables and a couple on the outside. Since it was raining on that day, the place was unfortunately pretty empty with us being the only guests in the beginning.
Unlike a lot of other places in this price category, they offer a 5- or 7-course menu which sounded exactly what I wanted for such a celebration! When ordering you also get to choose between a vegan or vegetarian option. The main difference is that two of the course also contain some cheese if you take the vegetarian route. Since I love cheese, I went that route!
And I wasn’t disappointed! You can find the whole menu here and I have to say, each course was delicious! My personal highlight was the greeting from the kitchen (second course) which was a grilled piece of zucchini with pumpkin seed oil, goat cheese, and roasted nuts/seeds! I just love those little surprise courses from the kitchen 😍
To summarise: I can absolutely recommend “Die Speis am Lendhafen” if you want to have a fancy vegetarian dinner! They also have a lot of other meals on their menu so I’m pretty sure there should be something for you unless you absolutely want to at some meat 😅
I want to configure my project on GitHub to require a whole pipeline run to pass before it can be merged. This is supported through “Branch protection rule” where you have to manipulate two settings:
While the first is quite straight forward, the second requires a bit of explanation. If you enable this flag then you have to specify one or more “statuses” that should be successful before the PR can be merged. A status is more or less just an indicator that you can attach to a commit through the GitHub API.
But does this mean that I have to write a custom action in my pipeline that manipulates such a status? No! Every job name in your pipelines becomes its own status. So if I now have a workflow with the name “CI” and a job with the name “main”, then I can pick “main” from the select box there and make it mandatory.
This has the side-effect that I should probably rethink how I name the jobs in my workflows…