skip to content

A Python service builds and runs fine on a Debian-based image, but after someone switches the Dockerfile's FROM instruction to an Alpine base the build gets much slower and a native library fails to load at runtime. Explain what is going on and how you would decide whether to stay on Alpine.

level: seniorimportance: should knowfreq 45%

answer

  1. Alpine = musl, Debian/Ubuntu = glibc
  2. manylinux wheels skipped -> source builds -> slow
  3. glibc-built .so will not load under musl
  4. musl: small thread stack, different DNS, slower allocator
  5. -slim keeps glibc and most of the size win

basics

~20 s

Alpine uses musl libc instead of glibc, so precompiled binaries and Python manylinux wheels built for glibc do not apply. Package managers fall back to compiling from source, which is slow and needs toolchains, and mismatched native libraries fail to load. Unless you have verified the native dependency chain, prefer a glibc -slim base.

solid answer

~60 s

Alpine's libc is **musl**, not glibc. Most prebuilt native artifacts in the ecosystem target glibc: Python manylinux wheels, many Node native modules, some JVM agents and profilers. On Alpine, pip cannot use those wheels (it needs `musllinux` wheels, which fewer projects publish), so it falls back to building from source, which is why the build got slow and why it now needs gcc and headers. The runtime failure is the same root cause: a shared object linked against glibc symbols will not resolve under musl. Beyond linking, musl historically differs in DNS resolution behaviour and uses a much smaller default thread stack size, which has bitten Python, Rust and JVM workloads. My decision rule: Alpine is a fine choice for statically linked Go or Rust, or for pure-Python stacks with musl wheels available. For anything with a heavy native dependency chain, I move to `python:3.12-slim`: same glibc as Debian, most of the size win, and none of the compatibility risk. I would not trade a couple of dozen megabytes for an unverifiable native ABI story.

code

dockerfile · 6 lines
dockerfile
FROM python:3.12-slim AS build
RUN pip install --prefix=/install psycopg2-binary

FROM python:3.12-alpine
COPY --from=build /install /usr/local
CMD ["python", "-c", "import psycopg2"]

go deeper

for a junior

Know that Alpine uses a different C library called musl and that this can break precompiled native code.

for a middle

Explain the wheel/ABI mechanism concretely: manylinux versus musllinux, source builds, symbol resolution failures at dlopen.

for a senior

Diagnose it: identify libc as the variable, mention thread stack, DNS and allocator differences that appear only under load, and give a decision rule per language runtime.

for a principal

Treat base image family as a platform standard: pick one runtime libc for the fleet, make exceptions explicit and measured, and weigh operational consistency above marginal image size.

## Two different C libraries Almost every Linux program links against a C library that implements the standard functions and the syscall wrappers. Mainstream distributions use **glibc** (GNU C Library). Alpine Linux uses **musl**, a small, strictly standards-focused implementation. Both are real, correct libcs, but they are different implementations with different binary interfaces for some behaviours and, critically, a different dynamic loader path and symbol set. That matters because the ecosystem distributes precompiled native code. Python wheels tagged manylinux are built against an old glibc so they run on essentially any glibc distro. Alpine does not match that tag, so pip skips those wheels and falls back to an sdist, compiling numpy, cryptography or psycopg from source. That explains the slow build: you are now compiling C and Rust, and you must install build-base, headers and possibly Rust toolchains, which ironically inflates the build image. Wheels for musl exist under the musllinux tag, but coverage is thinner than manylinux. The runtime failure is the same mechanism at a different moment. A shared object that was compiled and linked against glibc references symbols and loader behaviour musl does not provide, so dlopen fails or the process aborts with a symbol lookup error. Copying a glibc-built binary into an Alpine image is the classic version of this bug, and it appears often in multi-stage builds where the build stage is Debian and the runtime stage is Alpine. ## Other musl and Alpine differences - **Thread stack size.** musl's default per-thread stack is far smaller than glibc's. Deeply recursive code or runtimes that assume a large stack can crash only under load, which makes the failure look unrelated to the base image. - **DNS behaviour.** musl's resolver historically differed in handling of search domains, TCP fallback for large responses and parallel A/AAAA queries. In Kubernetes, where the resolv.conf carries multiple search domains, this has produced intermittent resolution failures. - **Memory allocator.** musl's allocator is optimised for size, not multithreaded throughput; some allocation-heavy multithreaded workloads measure meaningfully slower on Alpine. - **Tooling.** BusyBox provides trimmed versions of common utilities, so shell scripts relying on GNU-specific flags of sed, date or tar can break. ## How to decide Start from what you actually ship. 1. **Statically linked Go or Rust with no cgo**: libc is irrelevant at runtime, so Alpine, distroless-static or scratch are all fine. Alpine buys you a shell for debugging at a few megabytes. 2. **Pure interpreted code with no native extensions**: Alpine is usually fine; verify build time. 3. **Native extension chains (Python data stack, Node modules with prebuilds, JVM with native agents)**: prefer a glibc base. The -slim Debian variants keep glibc and still cut most of the size. Measure the actual benefit before paying the risk. A Python image on python:3.12-slim versus python:3.12-alpine often ends up similar in size once you have installed the compilers Alpine needed, and sometimes Alpine ends up larger. That is the punchline many candidates miss: Alpine's headline 5MB applies to the base, not to your finished image. If you do stay on Alpine, be deliberate: keep build and runtime stages on the same libc so nothing is copied across an ABI boundary, prefer musllinux wheels or apk packages of native libraries, and load test rather than smoke test, because the stack size and allocator differences show up under concurrency rather than in a health check. Finally, treat this as a fleet consistency question. Mixing libcs across services multiplies the number of environments your platform team must reason about, and produces bugs that reproduce in one service and not another. Standardising on one runtime base family and making exceptions consciously is usually worth more than the megabytes.

  • Your team still wants Alpine for the size win. What must be true for that to be safe?
    Build and run on the same libc so nothing crosses an ABI boundary, and confirm every native dependency has a musllinux wheel or an apk package rather than being compiled ad hoc in the image. Then load test rather than smoke test, because the musl thread stack size and allocator differences surface under concurrency. Finally compare final image sizes: once the compilers and headers Alpine needed are counted, the size advantage often evaporates.
  • A JVM service on Alpine intermittently fails DNS lookups inside Kubernetes but the same image works locally. Where would you look?
    Kubernetes injects a resolv.conf with several search domains and ndots settings, and musl's resolver handles search domains, parallel A/AAAA queries and TCP fallback differently from glibc, so queries that work with a single search domain fail in cluster. Reproduce with the cluster's resolv.conf, check whether responses exceed the UDP limit and require TCP fallback, and compare against the same image on a glibc base to isolate the libc as the variable.

musl and glibc are like two power grids running the same voltage but different plug shapes: your appliance is not broken, it simply cannot be plugged in without an adapter or a rebuild.

saying these in an interview costs you the question

  • Saying Alpine is smaller so it is strictly better, with no mention of musl
  • Assuming a binary compiled on Debian can simply be copied into an Alpine runtime stage
  • Thinking the slow build is a pip cache problem rather than source compilation of missing wheels
  • Claiming musl and glibc are binary compatible because both are POSIX
  • Believing the final Alpine image is always smaller, ignoring the toolchain it had to add

context