DEV Community

Pablo Lagos
Pablo Lagos

Posted on

Fixing “GLIBC_x.x not found” in Go Binaries: Build Once, Run Anywhere

“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'"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

✅ 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
Enter fullscreen mode Exit fullscreen mode

Or check dynamic dependencies:

ldd myapp
# Should say: "not a dynamic executable"
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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)