Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ ARG GO_VERSION=1.26
ARG XCPUTRANSLATE_VERSION=v0.9.0
ARG GOLANGCI_LINT_VERSION=v2.11.4
ARG MOCKGEN_VERSION=v0.6.0
ARG BUILDPLATFORM=linux/amd64
# Note: BUILDPLATFORM and TARGETPLATFORM are provided by BuildKit. BUILDPLATFORM must NOT
# be declared with a default value here: a declared default overrides the value BuildKit
# injects, which pins every build stage to that platform. With a linux/amd64 default, a
# native build on an arm host (armhf/armv7/arm64) pulls the amd64 toolchain images and
# fails with `exec /bin/sh: exec format error`.

FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
Expand Down
65 changes: 65 additions & 0 deletions build-armhf.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/bin/sh
# Build the gluetun image for 32 bit ARM (armhf).
#
# Usage:
# ./build-armhf.sh # build linux/arm/v7, load into the local docker
# PLATFORM=linux/arm/v6 ./build-armhf.sh
# TAG=myrepo/gluetun:armhf PUSH=1 ./build-armhf.sh
# ./build-armhf.sh --setup-qemu # register the QEMU handlers first (cross build only)
#
# Notes:
# - linux/arm/v7 is what Debian/Raspberry Pi OS 32 bit calls armhf; Alpine calls it armv7.
# Alpine's own "armhf" is ARMv6 (Pi 1 / Pi Zero): use PLATFORM=linux/arm/v6 for those.
# - Cross building from an x86 host needs the QEMU binfmt handlers, because the final
# image stage runs `apk add` for the target architecture. Building natively on the ARM
# host needs nothing extra.
set -eu

PLATFORM="${PLATFORM:-linux/arm/v7}"
TAG="${TAG:-gluetun:armhf}"
PUSH="${PUSH:-0}"

if [ "${1:-}" = "--setup-qemu" ]; then
echo "==> Registering QEMU binfmt handlers (needs a privileged container)"
docker run --privileged --rm tonistiigi/binfmt --install arm
shift
fi

host_arch="$(uname -m)"
case "${host_arch}" in
arm* | aarch64) native=1 ;;
*) native=0 ;;
esac

if [ "${native}" = "0" ] && ! ls /proc/sys/fs/binfmt_misc/ 2>/dev/null | grep -q 'qemu-arm'; then
echo "==> Host is ${host_arch} and no qemu-arm binfmt handler is registered."
echo " Cross building ${PLATFORM} will fail with 'exec format error' in the final stage."
echo " Run: ./build-armhf.sh --setup-qemu (or: docker run --privileged --rm tonistiigi/binfmt --install arm)"
exit 1
fi

# buildx is required for --platform; the default docker driver cannot build for
# another platform, so make sure a container driver builder exists.
if ! docker buildx inspect gluetun-builder >/dev/null 2>&1; then
echo "==> Creating buildx builder 'gluetun-builder'"
docker buildx create --name gluetun-builder --driver docker-container >/dev/null
fi

output="--load"
[ "${PUSH}" = "1" ] && output="--push"

VERSION="${VERSION:-$(git describe --tags --always --dirty 2>/dev/null || echo unknown)}"
COMMIT="${COMMIT:-$(git rev-parse --short HEAD 2>/dev/null || echo unknown)}"
CREATED="${CREATED:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}"

echo "==> Building ${TAG} for ${PLATFORM} (version=${VERSION} commit=${COMMIT})"
exec docker buildx build \
--builder gluetun-builder \
--platform "${PLATFORM}" \
--build-arg VERSION="${VERSION}" \
--build-arg COMMIT="${COMMIT}" \
--build-arg CREATED="${CREATED}" \
--tag "${TAG}" \
${output} \
"$@" \
.
72 changes: 72 additions & 0 deletions doc/build-armhf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Building for 32 bit ARM (armhf)

## What was failing

```
=> ERROR [base 3/10] RUN apk --update add git g++ findutils
1.271 exec /bin/sh: exec format error
```

The `base` stage — the Go toolchain stage — is pinned to the *build* platform:

```dockerfile
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
```

`BUILDPLATFORM` is a built-in build argument that BuildKit fills in with the platform of the
machine running the build. The Dockerfile used to also declare it with a default:

```dockerfile
ARG BUILDPLATFORM=linux/amd64
```

A declared default **wins over the value BuildKit injects**, so every stage above was pinned to
`linux/amd64` regardless of where the build ran. Building natively on an armhf host therefore
pulled the amd64 `golang` image and the first `RUN` in it died with `exec format error`. The
declaration has been removed, so `BUILDPLATFORM` is now the real build platform again.

## Platform naming

| You want | Docker platform | Alpine name | Typical hardware |
| --- | --- | --- | --- |
| armhf as Debian/Raspberry Pi OS 32 bit means it | `linux/arm/v7` | `armv7` | Pi 2/3/4/5 on a 32 bit OS |
| armhf as Alpine means it | `linux/arm/v6` | `armhf` | Pi 1, Pi Zero/Zero W |

## Building

Either use the helper script:

```sh
./build-armhf.sh # linux/arm/v7, loaded into the local docker
PLATFORM=linux/arm/v6 ./build-armhf.sh # ARMv6
TAG=you/gluetun:armhf PUSH=1 ./build-armhf.sh
```

or call buildx directly:

```sh
docker buildx build --platform=linux/arm/v7 -t gluetun:armhf --load .
```

### Natively on the ARM host

Nothing else is needed: the Go toolchain runs on the host, the Go binary is built for the host,
and the final Alpine stage runs natively. It is just slow, and `go build` wants roughly 1 GB of
free RAM (add swap on a 1 GB Pi).

### Cross building from an x86 host

The Go part is a true cross compile and needs no emulation, but the final image stage runs
`apk add` for the target architecture, so the QEMU binfmt handlers must be registered on the
host once per boot:

```sh
docker run --privileged --rm tonistiigi/binfmt --install arm
```

Without it the build fails with the same `exec format error`, but in the *last* stage rather than
in `base`. `./build-armhf.sh --setup-qemu` does the registration for you, and the script refuses
to start a cross build when the handler is missing instead of failing halfway through.

`docker buildx build --platform=<other platform>` also requires a container driver builder;
the default `docker` driver cannot do it. The script creates a `gluetun-builder` for that.