I kept hitting the same two errors on builds that worked a minute earlier:
version `GLIBC_2.34' not found
version `GLIBCXX_3.4.29' not found
The advice everywhere is "copy the library from the machine where it works". I tried
it properly, with a real build, and it fails one layer deeper than you expect. That
sent me looking for an actual table of what each distribution ships — and none
existed, so I measured one.
What I measured
For each release: ldd --version for glibc, and the highest GLIBCXX_* symbol
version inside libstdc++.so.6. Every row came out of the official image, not from
memory. 27 releases, each image removed immediately afterwards.
| Distribution | Release | glibc | musl | Highest GLIBCXX |
|---|---|---|---|---|
| Ubuntu | 16.04 | 2.23 | — | 3.4.21 |
| Ubuntu | 18.04 | 2.27 | — | 3.4.25 |
| Ubuntu | 20.04 | 2.31 | — | 3.4.28 |
| Ubuntu | 22.04 | 2.35 | — | 3.4.30 |
| Ubuntu | 24.04 | 2.39 | — | 3.4.33 |
| Ubuntu | 25.04 | 2.41 | — | 3.4.34 |
| Debian | 10 | 2.28 | — | 3.4.25 |
| Debian | 11 | 2.31 | — | 3.4.28 |
| Debian | 12 | 2.36 | — | 3.4.30 |
| Debian | 13 | 2.41 | — | 3.4.33 |
| Alpine | 3.20 | — | 1.2.5 | — |
| CentOS | 7 | 2.17 | — | 3.4.19 |
| Rocky Linux | 8 / 9 | 2.28 / 2.34 | — | 3.4.25 / 3.4.29 |
| AlmaLinux | 8 / 9 | 2.28 / 2.34 | — | 3.4.25 / 3.4.29 |
| Amazon Linux | 2 / 2023 | 2.26 / 2.34 | — | 3.4.24 / 3.4.33 |
| openSUSE Leap | 15.6 | 2.38 | — | 3.4.34 |
| Arch Linux | rolling | 2.44 | — | 3.4.36 |
Full 27-row table and the method: readelf.com/guides/glibc-version-by-distribution
The trap nobody mentions
Look at Rocky 9: glibc 2.34, but only GLIBCXX 3.4.29. A C++ binary built on
Ubuntu 22.04 (GLIBCXX 3.4.30) satisfies every glibc requirement there and still
refuses to start. Two independent version gates, and people only check one.
Why copying libstdc++.so.6 does not work
This is the part worth testing rather than repeating. Built on ubuntu:24.04 with
g++ 13.3:
$ ./hello-new
built with 13.3.0
Same binary on debian:12:
$ ./hello-new
./hello-new: libstdc++.so.6: version `GLIBCXX_3.4.32' not found
./hello-new: libstdc++.so.6: version `GLIBCXX_3.4.31' not found
Now copy the newer library across, keeping the SONAME:
$ cp /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.33 ./lib/libstdc++.so.6
$ LD_LIBRARY_PATH=./lib ldd ./hello-new | grep stdc++
libstdc++.so.6 => ./lib/libstdc++.so.6 # the copy IS used
$ LD_LIBRARY_PATH=./lib ./hello-new
./hello-new: libc.so.6: version `GLIBC_2.38' not found
(required by ./lib/libstdc++.so.6)
The loader picked the bundled library, and it still failed — because that newer
libstdc++ was itself built against glibc 2.38, and Debian 12 has 2.36.
The C++ runtime and the C runtime move as a pair. Pulling one forward drags the
other with it.
What actually works
- Build on the oldest image that still compiles your code. One command, no guessing:
docker run --rm -v "$PWD":/src -w /src debian:10 g++ -O2 -o app app.cpp
- Link the C++ runtime statically — removes the GLIBCXX requirement entirely (glibc stays dynamic, so you still need a compatible glibc):
g++ -O2 -static-libstdc++ -static-libgcc -o app app.cpp
- Go fully static against musl when the binary has to run anywhere:
docker run --rm -v "$PWD":/src -w /src alpine:3.20 \
sh -c 'apk add --no-cache g++ && g++ -O2 -static -o app app.cpp'
- Match the target image if it only ever runs in a container.
Checking a binary without running it
readelf --version-info ./app prints the required symbol versions, and
grep -aoE 'GLIBCXX_3\.4\.[0-9]+' /usr/lib/x86_64-linux-gnu/libstdc++.so.6 | sort -V | tail -1
prints what the host offers. Compare the two and you know before deploying.
I also built a browser-based ELF inspector that reads the same fields without
uploading anything: https://readelf.com/tools/elf-inspector/ — useful on jump
hosts where binutils is not installed (a slim debian:12 image has neither file
nor readelf).
Measured 2026-11-14 on x86-64. Corrections welcome — if a row is wrong for your
distribution, that is a bug and I want to know.
Top comments (0)