A complete guide to building and releasing binaries for AMD64 and ARM64, securing containers, and generating SBOMs.
In modern software development, multi-architecture support is becoming a standard requirement. Whether you’re targeting ARM64 environments, like Apple’s M1/M2 chips, or traditional AMD64 infrastructure, automating multi-architecture builds ensures consistent deployment across different platforms.
At ContainerInfra, we use GoReleaser and GitLab CI to automate the creation of Docker images and binaries for both AMD64 and ARM64. This setup supports macOS and Linux systems, allowing our developers to work across multiple environments without the need for manual configuration or additional tools.
The growing use of ARM64—driven by energy-efficient ARM-based devices and Apple’s M1/M2 systems—has made cross-platform builds essential. Here’s why supporting both AMD64 and ARM64 matters:
With native support for ARM64 systems, developers using Apple Silicon devices can build and test locally without workarounds or emulation. Native builds on ARM64 devices, such as Apple’s M1/M2, take full advantage of the hardware’s architecture, resulting in faster build times and more efficient local testing compared to emulated environments. This means developers can stay focused on coding and testing without being bogged down by issues related to compatibility or other differences between local and production environments.
Supporting multiple architectures not only ensures your software remains compatible as ARM64 adoption increases, but it also aligns with broader sustainability goals. ARM64 processors are designed to be more energy-efficient than traditional x86_64 (AMD64) processors, which means they consume less power while delivering comparable or even superior performance in certain workloads.
ARM64-based devices, such as Apple’s M1/M2 and ARM-based cloud instances, can significantly lower the energy footprint of your applications. This is particularly impactful for large-scale or edge deployments. More energy-efficient infrastructure translates to lower operational costs, especially for compute-heavy workloads.
Adapting your software to support both ARM64 and AMD64 prepares it for the growing shift toward sustainable, energy-conscious infrastructure without sacrificing compatibility or performance.
GoReleaser is a versatile tool for automating the release of Go projects, covering tasks such as building binaries, generating changelogs, packaging, and containerizing applications. When integrated with GitLab CI, it creates a fully automated pipeline for building and releasing multi-architecture Docker images.
At ContainerInfra, we use GoReleaser in combination with GitLab CI to generate both AMD64 and ARM64 container images as part of our CI/CD process. This setup offers several advantages:
By automating the build and release process, we improve efficiency while ensuring that releases are reproducible and comply with security and compliance standards. We’ve also added steps such as container signing with Cosign and SBOM generation to enhance transparency and strengthen our security practices.
The first step is configuring GoReleaser in the GitLab CI pipeline. GoReleaser automates the process of building, packaging, and releasing your Go projects, but with Docker’s Buildx, it also enables multi-architecture builds.
We start by creating a basic .goreleaser.yml
configuration file, which defines how GoReleaser interacts with GitLab and handles the build process.
Below is a basic GoReleaser configuration for GitLab CI:
version: 2
gitlab_urls:
api: https://gitlab.com/api/v4/
download: https://gitlab.com
This configuration sets up the API and download endpoints for GitLab, allowing GoReleaser to communicate with your GitLab repository.
With GoReleaser, you can build Docker images for multiple architectures, such as amd64 and arm64, using Docker’s buildx. Here’s how we configure multi-arch builds:
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
- arm64
In this configuration, GoReleaser builds binaries for both amd64 and arm64 architectures across linux and darwin platforms. This is important for projects that need to run across different environments, such as cloud infrastructure and Apple’s M1 architecture.
For Docker images, we define templates for each architecture:
dockers:
- image_templates: ["registry.gitlab.com/yourgroup/{{ .ProjectName }}:{{ .Version }}-amd64"]
use: buildx
dockerfile: Dockerfile
goos: linux
goarch: amd64
build_flag_templates:
- "--platform=linux/amd64"
- image_templates: ["registry.gitlab.com/yourgroup/{{ .ProjectName }}:{{ .Version }}-arm64v8"]
use: buildx
dockerfile: Dockerfile
goos: linux
goarch: arm64
build_flag_templates:
- "--platform=linux/arm64/v8"
These image_templates generate Docker images for both architectures. The use: buildx
directive instructs Docker to use the buildx
builder, which allows cross-compilation and multi-platform builds. The images are pushed to GitLab’s registry, tagged by architecture and version.
Once the images for both architectures are built, we use Docker manifests to create a single tag that references both images. This ensures that when users or services pull the image, Docker will automatically serve the correct architecture-specific image based on the pulling platform.
docker_manifests:
- name_template: "registry.gitlab.com/yourgroup/{{ .ProjectName }}:{{ .Version }}"
image_templates:
- "registry.gitlab.com/yourgroup/{{ .ProjectName }}:{{ .Version }}-amd64"
- "registry.gitlab.com/yourgroup/{{ .ProjectName }}:{{ .Version }}-arm64v8"
This makes it easier for developers and deployment tools to pull the right image without needing to manually specify the architecture.
Security is a key aspect of any release pipeline, and signing Docker images helps ensure their integrity. We use Cosign (part of the Sigstore project) to sign our Docker images as part of the GoReleaser workflow.
docker_signs:
- artifacts: all
args: ["sign", "--key=${COSIGN_KEY}", "--tlog-upload=false", "-a", "builder=gitlab-promote", "${artifact}@${digest}"]
This configuration signs all the Docker images that are built, ensuring that every image deployed can be verified for authenticity. We pass the signing key via an environment variable (COSIGN_KEY
), which allows the signing process to be integrated seamlessly into the CI pipeline.
In addition to signing images, creating a Software Bill of Materials (SBOM) is important for compliance and security auditing. SBOMs provide a detailed list of all components and dependencies included in the software, which is essential for understanding potential vulnerabilities. It can also be used for searching across a large amount of software components to identify what software dependencies and which versions or licenses are used.
We generate SBOMs using Syft as part of the GoReleaser build process:
builds:
- id: "platform"
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
hooks:
before:
- syft registry.gitlab.com/yourproject/{{ .ProjectName }}:{{ .Version }} -o spdx-json > ./sbom.json
This setup calls syft to generate an SBOM in the SPDX format for the Docker image. The resulting sbom.json file can be stored alongside the image artifacts, providing a detailed breakdown of dependencies and their licenses.
Now that we’ve configured GoReleaser, Docker Buildx, Cosign, and SBOM generation, it’s time to automate the entire process with GitLab CI. Here’s a simplified GitLab CI configuration that ties everything together:
stages:
- build
- release
build:
script:
- goreleaser release --rm-dist
only:
- tags
This pipeline automatically triggers the GoReleaser process when there’s a commit to the main branch. The --rm-dist
flag ensures that previous builds are cleaned up, preventing contamination between releases.
The entire process—from building the binaries, creating multi-architecture Docker images, signing those images, and generating SBOMs—is fully automated, saving both time and reducing human error.
Our GitLab CI setup revolves around two core templates:
golang.yaml
): This handles general Go builds using Docker and supports the necessary authentication for accessing private Git repositories and Docker registries.Here’s a quick look at the configuration:
Our CI variables set-up support for GitLab’s container registry and secure access to private repositories for go dependencies:
variables:
GOPATH: $CI_PROJECT_DIR/.go
REGISTRY_USERNAME: gitlab-ci-token
REGISTRY_PASSWORD: $CI_JOB_TOKEN
REGISTRY_NAME: $CI_REGISTRY
GITLAB_TOKEN: $YOUR_GITLAB_ENV_TOKEN
NETRC: machine gitlab.com login gitlab-ci-token password $GITLAB_TOKEN
GOPRIVATE: gitlab.com
These variables allow the GitLab pipeline to authenticate with both the registry and private Go modules during the build process.
In our GitLab CI pipeline, the .go:goreleaser
template automates the process of building and releasing Go projects with GoReleaser. This template handles multi-architecture Docker builds, ensuring that everything from cross-compilation to image pushing happens seamlessly. Let’s break down each component of the template:
.go:goreleaser:
extends:
- .go:release
- .docker
image:
name: goreleaser/goreleaser:v2.3.2
entrypoint: ['']
services:
- name: docker:26-dind
only:
- tags
script:
- echo $NETRC > ~/.netrc
- docker run --privileged --rm tonistiigi/binfmt:qemu-v6.2.0 --install all
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- goreleaser release --clean --skip-validate
The template extends two core configurations: .go:release and .docker.
For building Docker images within the pipeline, we rely on Docker-in-Docker (DinD). Additionally, in the script section, there are a few important steps.
We configure .netrc
to allow access to private Go repositories during the build process. We also install QEMU to enable cross-compilation, allowing us to build Docker images for multiple architectures, like AMD64 and ARM64, using Docker Buildx.
Integrating GoReleaser with GitLab CI has allowed us to standardize our build and release process across different architectures, ensuring consistency and reliability in every release.
As a cloud-native MSP, this integration has not only streamlined our internal workflows but also enhanced our ability to support customers in optimizing their CI/CD pipelines. The standardized approach enables us to deliver secure, multi-architecture builds that are both reproducible, while implementing SBOMs and signatures to improve security.