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

  1. 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 
  1. 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 
  1. 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' 
  1. 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.