Post

Rust binaries with Diesel and Postgres: static linking on 2025

Rust binaries with Diesel and Postgres: static linking on 2025

TLDR

See the summarizing section.

Introduction

I have been writing Rust code in a professional manner since 2019. The “de-facto” ORM for Rust on that time was Diesel, and I have been using it since then. The software that I was writing was meant to run on almost any Linux system, while being distributed as pre-compiled binaries. The easiest way to achieve that is to statically link the binaries, so they don’t depend on any shared libraries that may or may not be present on the target system.

The problem

Rust is really good at static linking. Avoiding the (g)libc problem is as easy as using musl as the target. For OpenSSL, it’s as easy as using the vendored feature. But the projects that I was working on, also used Diesel with Postgres as the database backend. Diesel uses pq-sys crate as the low-level binding to the Postgres C client library, libpq. And libpq is a nightmare when it comes to static linking due to its hard dependencies on other shared libraries, like OpenSSL, libssl, libcrypto, etc., so you need to bundle all of their static files built for your target.

That’s why custom docker containers like rust-musl-builder, rust_musl_docker, and others, were created. They provide a pre-configured environment with musl, OpenSSL, Postgres, and libpq statically linked, so you can build your Rust binaries with Diesel and Postgres support. They were hard to maintain, and often broke due to changes in the underlying libraries; as a result, they were abandoned by their maintainers and CI/CD pipelines that used them broke.

Note: yes, I know that there’s sqlx, which uses rustls instead of OpenSSL, and can be statically linked without any issues. But when you have more than 30 different projects using Diesel for many things, migrating them all to sqlx is not a small task.

The solution

As I mentioned before, there used to exist some docker images that provided a pre-configured environment for building Rust binaries with Diesel and Postgres support, big kudos to their maintainers, they worked very well for many years. After the CI/CD fails, I started to investigate if there was another way to achieve the same result without resorting to forking and maintaining a custom docker image.

As mentioned above, the main problem is libpq. You can spin up an Alpine Linux container, install the required dependencies (build-base pkgconfig postgresql-dev libbsd-dev openssl-dev zlib-dev zlib-static perl git in my case), but you will still hit the libpq wall:

{ % raw % }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
error: linking with `cc` failed: exit status: 1
  |
  = note:  "cc" "-m64" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/rcrt1.o" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/crti.o" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/crtbeginS.o" "/tmp/rustchvPyBK/symbols.o" "<1 object files omitted>" "-Wl,--as-needed" "-Wl,-Bstatic" "-lpq" "/tmp/rustchvPyBK/{libzstd_sys-6c0886af15f7a887.rlib,libopenssl_sys-ccd5a7f01478a311.rlib}.rlib" "-lunwind" "-lc" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/{libcompiler_builtins-*}.rlib" "-L" "/tmp/rustchvPyBK/raw-dylibs" "-Wl,-Bdynamic" "-Wl,--eh-frame-hdr" "-Wl,-z,noexecstack" "-nostartfiles" "-L" "/build/target/x86_64-unknown-linux-musl/release/build/zstd-sys-ba1068f06e2ad027/out" "-L" "/build/target/x86_64-unknown-linux-musl/release/build/openssl-sys-8fb0ce292de28293/out/openssl-build/install/lib" "-L" "/usr/lib" "-L" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained" "-L" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib" "-o" "/build/target/x86_64-unknown-linux-musl/release/deps/test_api-fd10fed5f8242f8f" "-Wl,--gc-sections" "-static-pie" "-Wl,-z,relro,-z,now" "-Wl,-O1" "-Wl,--strip-all" "-nodefaultlibs" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/crtendS.o" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/crtn.o"
  = note: some arguments are omitted. use `--verbose` to show all linker arguments
  = note: /usr/lib/gcc/x86_64-alpine-linux-musl/14.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: /usr/lib/libpq.a(fe-connect.o): in function `defaultNoticeProcessor':
          fe-connect.c:(.text+0x6a8): undefined reference to `pg_fprintf'
          /usr/lib/gcc/x86_64-alpine-linux-musl/14.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: /usr/lib/libpq.a(fe-connect.o): in function `sslVerifyProtocolVersion':
          fe-connect.c:(.text+0x6d1): undefined reference to `pg_strcasecmp'
          /usr/lib/gcc/x86_64-alpine-linux-musl/14.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: fe-connect.c:(.text+0x6e5): undefined reference to `pg_strcasecmp'
          /usr/lib/gcc/x86_64-alpine-linux-musl/14.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: fe-connect.c:(.text+0x6f9): undefined reference to `pg_strcasecmp'
          /usr/lib/gcc/x86_64-alpine-linux-musl/14.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: fe-connect.c:(.text+0x70d): undefined reference to `pg_strcasecmp'
          /usr/lib/gcc/x86_64-alpine-linux-musl/14.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: /usr/lib/libpq.a(fe-connect.o): in function `emitHostIdentityInfo':
          fe-connect.c:(.text+0x845): undefined reference to `pg_getnameinfo_all'
          /usr/lib/gcc/x86_64-alpine-linux-musl/14.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: /usr/lib/libpq.a(fe-connect.o): in function `connectFailureMessage':
          fe-connect.c:(.text+0x988): undefined reference to `pg_strerror_r'
...snip
          collect2: error: ld returned 1 exit status
          
  = note: some `extern` functions couldn't be found; some native libraries may need to be installed or have their path specified
  = note: use the `-l` flag to specify native libraries to link
  = note: use the `cargo:rustc-link-lib` directive to specify the native libraries to link with Cargo (see https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-lib)

error: could not compile `test_api` (bin "test_api") due to 1 previous error
Error: building at STEP "RUN cargo build --target x86_64-unknown-linux-musl --release --locked": while running runtime: exit status 101

{ % endraw % }

pq-sys 0.5.0

pq-sys 0.5.0 was released in early 2024, with a VERY handy feature:

  • We added a pq-src crate and a bundled feature for pq-sys. This allows to build and link a static version of libpq during the rust build process. This feature currently supports builds targeting Windows, Linux and macOS. It requires a c-compiler toolchain for the target to build libpq from source.

This solved all the headaches of dealing with libpq and its dependencies, as now you can just enable the bundled feature and everything builds just fine on an Alpine Linux container. I was really surprised that this solution is not found in the Diesel documentation, and almost nowhere on the internet.

Summarizing

So, summarizing, to build Rust binaries with Diesel and Postgres support, statically linked, you need something like this on your Cargo.toml:

1
2
3
4
5
6
7
8
9
10
11
12
13
[dependencies]
diesel = { version = "2.3.1", features = [
    "postgres",
    "r2d2",
    "time",
    "chrono",
    "uuid",
    "numeric",
    "network-address",
] }
#### Needed to build static binaries ####
pq-sys = { version = "0.7.2", features = ["bundled"] }
openssl-sys = { version = "0.9.109", features = ["vendored"] }

And then build your project targeting x86_64-unknown-linux-musl:

1
cargo build --target x86_64-unknown-linux-musl --release --locked

That’s it! You will get a fully statically linked binary that works on almost any Linux system.

Notes

According to the Diesel maintainers, the reason this solution is almost nowhere to be found is because it has only been implemented for a short time, compared to Diesel’s existence, which dates back to 2016.

Conclusion

I hope this small guide helps you to build your Rust binaries with Diesel and Postgres support, statically linked, without the need to maintain custom docker images or deal with the headaches of libpq and its dependencies. I will be sending a PR to the Diesel documentation to include this information, as I believe it will help many people in the Rust community.

This post is licensed under CC BY 4.0 by the author.