Want your Go app to “just run” on any Linux server, regardless of glibc?
You need real static linking — and it’s surprisingly easy to get wrong.
In this guide you’ll learn:
✅ Why CGO can break static builds
✅ How to cross-compile with musl
✅ How to do it inside Docker for guaranteed reproducibility
✅ How to declare all this in a clean .gobuilder.yml
⚙️ The problem: CGO and glibc versions
By default, Go wants to make static binaries. But the second you use CGO_ENABLED=1
(for SQLite, image processing, or networking optimizations), the linker pulls in your system’s libc — and suddenly you’ve got a dynamic dependency.
Run your binary on a server with an older glibc, and boom:
libc.so.6: version `GLIBC_2.32' not found
🫧 The solution: build with musl
and force static linking
Musl is a lightweight C standard library designed for static builds.
Here’s the secret sauce:
CGO_ENABLED=1 CC=musl-gcc \
go build -ldflags="-linkmode external -extldflags '-static'"
Check your binary:
file myapp # should say: statically linked
ldd myapp # should say: not a dynamic executable
✅ ✅ ✅
🗂️ Making this repeatable with go-builder
Manually remembering CGO_ENABLED
, CC
, and those scary -ldflags
? Painful.
Let’s lock it down:
# .gobuilder.yml
build_dir: builds
source: ./cmd/myapp
output: myapp
env:
CGO_ENABLED: "1"
CC: musl-gcc
build:
ldflags:
- "-linkmode external -extldflags '-static'"
vars:
main.version: "${VERSION:-dev}"
trimpath: true
verify_static: true # check with 'file' after build!
targets:
- os: linux
arch: amd64
- os: linux
arch: arm64
Run it:
VERSION=1.4.0 go-builder
- ✅ You get binaries under
builds/linux/amd64/
andbuilds/linux/arm64/
. - ✅ If the linker produces a dynamic binary by mistake, the
verify_static: true
will fail the build. - ✅ No bash scripts, no fragile Makefiles.
🐳 What if you don’t have musl-gcc
installed?
Use a container with musl
ready to go. Here’s a docker
block in the same YAML:
docker:
image: golang:1.23-alpine
workdir: /src
shell: sh
setup:
- apk add --no-cache musl-dev build-base git
env:
CGO_ENABLED: "1"
CC: gcc # gcc is the name of musl-gcc in alpine
Now your build runs inside Alpine Linux, with musl preinstalled. The builds/
folder is shared with your host, so your static binaries are there immediately — no extra copy step.
Run:
go-builder
And your machine stays clean: no local musl toolchain needed.
🧪 Dry-run: double-check before wasting time
Want to see exactly what go-builder
will run?
go-builder --dry-run
You’ll see:
- Docker
run
command (ifdocker:
is defined) - Environment variables
- The
go build
line, with every tag, flag, and linker trick.
✅ Key takeaways
✔ musl makes true static linking predictable
✔ CGO_ENABLED=1 CC=musl-gcc + -extldflags '-static'
are the magic trio
✔ verify_static: true
guarantees your binary is really static
✔ Using Docker means zero “it works on my machine” moments
✔ go-builder
locks your build config in YAML: repeatable, documented, and easy to share
🔗 Next steps
📄 Repo: github.com/pablolagos/go-builder
🧩 Install: go install github.com/pablolagos/go-builder@latest
⚡ --init
writes a starter YAML, ready to tweak.
Stop chasing glibc errors. Ship one binary, run it anywhere.
Static linking can actually be fun.
Do you ship static Go binaries? Drop your tips below — I'd love to hear how you do it!
Top comments (0)