“I built my shiny Go app. It works on my machine. But when I run it on a server — boom:
Error while loading shared libraries: libc.so.6: version 'GLIBC_2.32' not found
.”_
Sound familiar? Let’s break down why this happens, what it means, and how you can prevent it every time.
🤔 Why does GLIBC
bite you?
Go is famous for producing static binaries by default. But:
- If your code (or a dependency) uses CGO, Go will link to your system’s C libraries.
- On your dev machine, your libc might be GLIBC 2.35 (Ubuntu 22.04).
- But your production machine could be older — say, GLIBC 2.28 (Debian 10).
Result? Your binary demands symbols that the older server’s libc doesn’t have.
No fancy containerization will fix that if the binary expects to find libc.so.6
on the host!
✔️ 3 ways to make this error disappear
✅ 1. Use a musl-based toolchain
Musl is a minimal C library that’s explicitly designed for static linking.
For example:
CGO_ENABLED=1 CC=x86_64-linux-musl-gcc \
go build -ldflags="-linkmode external -extldflags '-static'"
Why this works:
- You ship zero dynamic libc dependency.
- Your binary works on any Linux kernel, no matter the host’s GLIBC version.
You can also build inside Alpine (which uses musl by default):
FROM golang:1.23-alpine
RUN apk add --no-cache musl-dev build-base
WORKDIR /src
COPY . .
RUN go build -tags netgo -ldflags="-linkmode external -extldflags '-static'" -o myapp ./cmd/myapp
✅ 2. Check that your binary is truly static
Don’t guess — verify!
Run file
on the binary:
file myapp
# Should say: ELF 64-bit LSB executable, statically linked
Or check dynamic dependencies:
ldd myapp
# Should say: "not a dynamic executable"
If you see libc.so.6
in the list: your binary is not fully static!
✅ 3. Use a minimal base image or scratch
If you package your binary in a container, you don’t want to drag along a full glibc runtime just for your app.
Examples:
# Option A: static binary → scratch
FROM scratch
COPY myapp /myapp
ENTRYPOINT ["/myapp"]
# Option B: Alpine → musl → always works
FROM alpine:3.19
COPY myapp /usr/local/bin/myapp
ENTRYPOINT ["myapp"]
No glibc, no runtime version mismatch.
⚡ Gotchas: When you can’t be fully static
- Some CGO dependencies can’t be statically linked easily (e.g., dynamic plugins, drivers, or certain SQLite features).
- In these cases, match your build environment to your production environment: same glibc, same version, same distro.
- Tools like Docker help, but musl or static linking is the long-term fix.
🧘 Stay sane: make it declarative
To avoid forgetting CGO_ENABLED
, CC
, or -extldflags
, keep your build config versioned:
# .gobuilder.yml
env:
CGO_ENABLED: "1"
CC: gcc
build:
ldflags:
- "-linkmode external -extldflags '-static'"
vars:
main.version: "${VERSION}"
tags: ["netgo"]
One file, repeatable builds, no surprises.
✅ Summary: one checklist for portable Go binaries
✔ Always verify: file
and ldd
✔ Use musl + static linking when possible
✔ Test your binary on your oldest target environment
✔ Don’t depend on your dev machine’s glibc!
Next time you see “version GLIBC_2.xx
not found”, you’ll know exactly what to do.
Happy compiling!
If you want a reproducible cross-build workflow with YAML and Docker baked in, check out go-builder — it keeps static linking predictable.
Top comments (0)