# Building release binaries in containers Release binaries must be built in a container based on an **old** Linux distribution. This document is written to be executed, by a person or an agent, on a machine that has nothing set up yet. --- ## 1. Why this exists glibc compatibility runs one way only. A binary linked against glibc 2.35 demands symbol versions that glibc 2.31 does not have, and refuses to start. A binary linked against glibc 2.29 runs on 2.29, 2.31 and 2.35 alike. Measured on the actual fleet, 2026-08-25: | binary | max GLIBC required | runs on | |---|---|---| | what all four 20.04 seeds run today (`v1.0.3-d159e7208`) | `GLIBC_2.29` | 18.04, 20.04, 22.04 | | anything built on seed 176 today (Ubuntu 22.04) | `GLIBC_2.34` | 22.04 only | The second binary will not start on four of our own five seeds. The loader reports: ``` /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found /lib/x86_64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.30' not found ``` Nothing new is being *called*. glibc 2.34 merged libpthread and libdl into libc and re-versioned every `pthread_*`, `dlsym` and `dladdr` symbol; 2.33 replaced the old `__xstat` inlines with real `stat`/`fstat`/`lstat64`. All of those functions exist in 2.31 under older tags. Building against older headers is the entire fix. **Do not try to solve this with full static linking.** The daemon calls `getaddrinfo`, `gethostbyname` and `getnameinfo`, and it must resolve `node1..node5.dragonx.is`, which are hard-coded and injected into `-addnode` on every node. Under a fully static glibc binary those go through NSS, which `dlopen`s `libnss_dns.so.2` at run time — it either fails or silently requires the target to have the same glibc you linked against, which defeats the purpose. --- ## 2. What already exists in this repo Do not write a new build system. Two pieces are already here: - **`Dockerfile.compat`** — an Ubuntu base image that installs the toolchain, copies the tree, **deletes any host-built `depends/` and object files**, runs `./util/build.sh`, and strips the three binaries. - **`./build.sh --linux-compat`** — builds that image, creates a throwaway container, copies `dragonxd`, `dragonx-cli` and `dragonx-tx` out into `release/dragonx--linux-amd64-ubuntu2004/`, adds `bootstrap-dragonx.sh`, `asmap.dat` and the two sapling params, fixes ownership, and prints the binary's maximum required GLIBC version. The base image is parameterised via `ARG BASE_IMAGE` (default `ubuntu:20.04`), so the same Dockerfile can target several glibc floors. --- ## 3. Prerequisites Docker (the scripted path uses `docker` specifically; podman works for the manual path if you alias or substitute it). ```sh sudo apt-get update sudo apt-get install -y docker.io git sudo usermod -aG docker "$USER" # then log out and back in, or every command needs sudo ``` Budget, measured on a 4-core box: | resource | needs | |---|---| | disk | ~15 GB free (the `depends/` tree alone is ~1.6 GB per target, plus image layers) | | RAM | 4 GB minimum, 8 GB comfortable — the link step is the peak | | time | **1–2 hours per base image on first build.** `depends/` builds boost, BDB, wolfssl, libevent, libsodium, libcurl and rust from source. Later builds reuse Docker layer cache unless the tree changed. | `depends/` downloads and builds its own rust toolchain, so the host's rust (or absence of it) is irrelevant. --- ## 4. Build one target ```sh git clone https://git.dragonx.is/DragonX/dragonx cd dragonx git checkout ./build.sh --linux-compat ``` Output lands in `release/dragonx--linux-amd64-ubuntu2004/` and the script prints the max GLIBC at the end. `` is read from `configure.ac`, not hardcoded, so it always matches what the binaries report. --- ## 5. Build several targets ```sh for BASE in ubuntu:18.04 ubuntu:20.04 ubuntu:22.04; do TAG="dragonx-compat-${BASE#ubuntu:}" TAG="${TAG//./}" docker build --build-arg "BASE_IMAGE=$BASE" -f Dockerfile.compat -t "$TAG" . OUT="release/dragonx-$(grep -oP 'define\(_CLIENT_VERSION_MAJOR, \K[0-9]+' configure.ac).$(grep -oP 'define\(_CLIENT_VERSION_MINOR, \K[0-9]+' configure.ac).$(grep -oP 'define\(_CLIENT_VERSION_REVISION, \K[0-9]+' configure.ac)-linux-amd64-${BASE#ubuntu:}" mkdir -p "$OUT" CID=$(docker create "$TAG") for b in dragonxd dragonx-cli dragonx-tx; do docker cp "$CID:/build/src/$b" "$OUT/$b"; done docker rm "$CID" >/dev/null cp util/bootstrap-dragonx.sh contrib/asmap/asmap.dat sapling-output.params sapling-spend.params "$OUT/" 2>/dev/null || true done ``` ### Which base to choose | base | glibc it provides | default GCC | verdict | |---|---|---|---| | `ubuntu:18.04` | 2.27 | 7 | **Verify before relying on it.** The tree is built with `-std=c++17`; GCC 7's C++17 support is incomplete and its cmake (3.10) may be too old for RandomX. Attempt only if you need to reach 18.04 users, and treat a successful build as the proof. | | `ubuntu:20.04` | 2.31 | 9 | **Recommended floor.** GCC 9 covers C++17 fully. Evidence it works: the binary the fleet runs today requires only `GLIBC_2.29`, i.e. the code touches nothing newer, so a 20.04 build reaches 18.04 machines anyway. | | `ubuntu:22.04` | 2.35 | 11 | **Do not ship this.** It is what we already have and what excludes four of our own seeds. Useful only for development. | Ubuntu 20.04 left standard support in April 2025, which is precisely why it belongs in a container on a patched host rather than on a build box someone has to maintain. --- ## 6. Verify — this step is not optional A build that silently targets the wrong glibc looks completely normal until a user reports that nothing starts. ```sh BIN=release/dragonx--linux-amd64-ubuntu2004/dragonxd # The ceiling. Must be <= the glibc of the OLDEST system you intend to support. objdump -p "$BIN" | grep -oE 'GLIBC_2\.[0-9]+' | sort -t. -k2 -n | tail -1 objdump -p "$BIN" | grep -oE 'GLIBCXX_3\.4\.[0-9]+' | sort -t. -k3 -n | tail -1 # If the ceiling is too high, this names the symbols responsible. readelf --dyn-syms --wide "$BIN" | grep -E '@GLIBC_2\.(3[2-9])' ``` Expected for a 20.04 build: `GLIBC_2.29` or lower, `GLIBCXX_3.4.26` or lower. Then actually run it somewhere old. A ceiling check proves the loader will resolve the symbols; it does not prove the binary works. `./dragonxd --version` on a real 20.04 box is a ten-second confirmation. --- ## 7. Traps Each of these has cost real time. **`ETXTBSY` when installing over a running daemon.** `cp` onto the binary fails with "Text file busy" *even after the process has exited* — `pgrep` returning nothing is not sufficient, the kernel still holds the text mapping. Stage into the same directory and `mv` (rename is not blocked), allow ~10 s to settle, and **sha256-verify the installed file before starting it**. A failed copy that goes unnoticed leaves the old binary running and looks like a successful deploy. **Never touch `configure.ac` in a configured tree.** Even `cp`-ing back a byte-identical copy updates its mtime, which makes `make` regenerate `aclocal.m4` and `configure` and then re-run `configure`, which fails with `libdb_cxx headers missing` because the depends prefix is not on the command line. If it happens: confirm `git diff --quiet HEAD -- configure.ac`, then restore mtime order oldest-to-newest with one-second gaps — `configure.ac`/`Makefile.am`, then `aclocal.m4`, then `configure`/`Makefile.in`, then `config.status`, then `Makefile`. Inside a container this cannot happen, which is one more reason to build there. **Never blind-`touch` a path that might not exist.** `touch src/config/hush-config.h` silently *creates* an empty stray file; the real header is `bitcoin-config.h`. Check `git status` after any timestamp surgery. **RandomX must be built with `ARCH=default`.** `util/build.sh` already passes it and the comment there explains why: `ARCH=native` tunes to the build machine, and a build on an AVX-512 host emitted 746 `zmm` instructions into `librandomx.a`, which `SIGILL`s on the entire fleet. If you ever invoke cmake by hand, pass `-DARCH=default`. **Strip before distributing.** Unstripped is ~220 MB, stripped ~16 MB. `Dockerfile.compat` already strips inside the container. **`util/build-win.sh` discards every argument.** There is no `"$@"` handling in it, so `-j$(nproc)` and `--disable-tests` are dropped on the floor and the Windows build is single-threaded. Expect it to be far slower than you planned. **Windows also needs `-Wa,-mbig-obj` and `-DARCH=default`.** Both are in `util/build-win.sh` today. The mingw flag was missing from `dev` for a month; without it the cross-compile fails at link because boost-heavy translation units exceed the PE/COFF section limit. Do not lose it on a re-branch. **macOS cannot be containerised.** `util/build-mac.sh` is a native-Mac script, and there is no darwin cross-compile path in `depends/` at all: `hosts/darwin.mk` wants `native_cctools`, which has no package definition, and there is no SDK in the tree. It also hardcodes an Intel Homebrew GCC path, so it produces x86_64 only — no arm64, no universal binary. macOS needs a real Mac. **The `contrib/gitian-descriptors/` files are not a build path.** They are unmodified upstream Bitcoin files (`name: "bitcoin-win-0.11"`, suite `trusty`) with zero DragonX content. Ignore them. --- ## 8. Handoff checklist - [ ] Docker installed, user in the `docker` group, ~15 GB free - [ ] Correct tag or branch checked out, tree clean (`git status`) - [ ] Version in `configure.ac` is the one you intend to release - [ ] `./build.sh --linux-compat` completes - [ ] GLIBC ceiling is **2.31 or lower** (2.29 expected) - [ ] GLIBCXX ceiling is **3.4.28 or lower** (3.4.26 expected) - [ ] `dragonxd --version` runs on a real machine of the oldest supported distro - [ ] Binaries stripped, `release/` contains the bootstrap script, `asmap.dat` and both sapling params - [ ] sha256 recorded for each artifact One more thing that is not a build step but belongs in the same conversation: the in-app daemon updater refuses any release without a detached signature (`kDaemonRequireSignature = true`). Publishing checksums alone means no existing user can update in place.