You copy a dynamically linked binary that was built on Ubuntu onto an Alpine Linux machine. The file is present, owned correctly and executable, but running it prints `./server: not found`. Why does Alpine refuse to run it, and what are your options?
answer
- the file is there, something else isn't
- which loader does the executable ask for
- glibc loader path versus musl loader path
- readelf -l prints the requested interpreter
- rebuild, link statically, or gcompat
basics
~20 sAlpine ships musl libc, so the glibc dynamic loader the binary names in its ELF interpreter field is absent. The kernel's ENOENT refers to that missing loader, not to your file. Rebuild against musl, link statically, or install gcompat.
solid answer
~40 sAlpine's C library is musl, not glibc. Every dynamically linked ELF binary hard-codes the absolute path of the dynamic loader it wants in its `PT_INTERP` header: a glibc x86-64 build asks for `/lib64/ld-linux-x86-64.so.2`, which does not exist on Alpine, where the loader is `/lib/ld-musl-x86_64.so.1`. `execve` therefore fails with ENOENT for the *interpreter*, and the shell reports it as though the binary itself were missing — which is why the message is so misleading. Confirm it with `file` or `readelf -l`, both of which print the requested interpreter. The real fixes are: rebuild the program with a musl toolchain, link it fully statically (a Go build with `CGO_ENABLED=0` needs nothing else), install Alpine's `gcompat` shim, or simply use a glibc-based distribution. The shim covers many binaries but is not a complete glibc.
code
bash · 6 lines# On the Alpine host: which loader does the binary demand?
readelf -l ./server | grep -A1 INTERP
# [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
ls -l /lib/ld-musl-x86_64.so.1 # the only loader Alpine ships
apk add gcompat # shim that maps glibc expectations onto muslgo deeper
Know that Alpine uses musl instead of glibc, and that a binary built elsewhere may simply refuse to start. Say plainly that you would rebuild the program on Alpine rather than fight the error.
Explain the mechanism: the ELF PT_INTERP header names an absolute loader path, the kernel fails execve with ENOENT when it is missing, and the shell misreports it. Name both loader paths and show how file or readelf -l confirms it.
Show you can triage this in production: separate a missing interpreter from a missing library, judge when gcompat is acceptable versus when the base image must change, and know the glibc static-linking NSS caveat before you promise a static build.
Own the policy question. Decide whether the organisation standardises on a musl userland at all, given that every vendor binary, profiler and agent must then be musl-capable, and weigh that recurring integration tax against the footprint the choice buys.
## The error message is lying to you `not found` from a shell almost always means the path you typed does not exist. When you have just `ls`-ed the file and it is plainly there, the message is reporting a *different* missing path: the one baked inside the executable. Understanding that single indirection is the whole question, and it is the most common first day on Alpine Linux. ## What PT_INTERP is A dynamically linked ELF executable cannot start on its own. It carries a program header of type `PT_INTERP` holding the absolute path of a helper program — the dynamic loader — whose job is to map the executable, load every shared library it needs, resolve symbols, and jump to the entry point. When you `execve` such a file, the kernel reads `PT_INTERP`, and if that path does not resolve, the whole `execve` fails with ENOENT. The shell has no way to tell you which of the two paths was missing, so it prints the one it knows. On a glibc x86-64 system that path is `/lib64/ld-linux-x86-64.so.2`. On Alpine it is `/lib/ld-musl-x86_64.so.1`. Neither libc installs the other's loader, so a glibc-built binary dropped onto Alpine is asking for a file that was never there. ``` readelf -l ./server | grep -A1 INTERP # [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] ``` `file ./server` prints the same information more compactly, and both tools work on any distribution, so you can run the check on either side. ## Why Alpine is different in the first place Alpine builds its entire userland against musl, a small, permissively licensed libc written for correctness and size rather than for glibc compatibility. musl is not a drop-in ABI clone of glibc: it does not implement glibc's symbol versioning, it omits or behaves differently in a number of GNU extensions, and it does not provide glibc's Name Service Switch plugin mechanism. That is a deliberate design position, and it is the source of essentially every Alpine surprise — this loader error is just the loudest one. ## Distinguish the two failure modes There are two different musl/glibc failures and candidates conflate them: 1. **The interpreter is missing.** `execve` never succeeds; you get a bare `not found` from the shell. This is the case above. 2. **The interpreter runs but a library is missing or a symbol is unresolved.** Here musl's loader *did* start and prints its own diagnostic, typically `Error loading shared library libfoo.so.1: No such file or directory` or an unresolved-symbol message naming the symbol. That is a dependency problem, not an ABI-selection problem. Knowing which one you have tells you whether you need a different build or just another `apk add`. ## The options, honestly ranked **Rebuild against musl.** The clean answer. Compile on Alpine, or cross-compile with a musl toolchain. For most compiled languages this is routine; for Go it is a build tag away; for Rust there is a musl target triple. This is the only option that leaves you with a first-class binary. **Link statically.** A fully static binary has no `PT_INTERP` at all, so the host libc is irrelevant. Go with `CGO_ENABLED=0` produces exactly this and runs unmodified on Alpine. Note the trap on the other side: a *glibc* program linked with `-static` is still not truly self-contained, because glibc's `getaddrinfo` and user lookups load NSS modules with `dlopen` at run time, and glibc warns about it at link time. **Install `gcompat`.** Alpine packages a compatibility layer that makes many glibc-linked binaries work on musl; the older `libc6-compat` package provides the loader path symlinks. This is the pragmatic escape hatch for a vendor binary you cannot rebuild, but it is a shim: anything reaching for glibc-specific behaviour, versioned symbols or NSS can still fail, and you have now built your production story on a translation layer. **Use a glibc distribution.** Sometimes the right answer, and saying so is a sign of judgment rather than defeat. The whole reason to be on Alpine is a small footprint; if you are going to spend a week fighting the libc, the footprint was not worth it. ## Why interviewers ask this It separates people who have shipped on Alpine from people who have read that it is small. The answer requires knowing that a libc is an ABI and not just a package, that ELF records an absolute loader path, and that a confusing error message can be about a file you never typed.
- How would you tell this apart from a plain missing-shared-library problem?By who prints the error. A missing interpreter fails inside `execve`, so the shell reports `not found` before any loader runs. If the loader did start, musl prints its own message naming the library or symbol, such as `Error loading shared library libfoo.so.1`. Run `readelf -l` for the interpreter path and `readelf -d` for the `NEEDED` entries to confirm which layer failed.
- A Go binary built on Ubuntu runs fine on Alpine, but a second one from the same repo does not. What differs?Cgo. With `CGO_ENABLED=0` the Go toolchain emits a fully static binary with no ELF interpreter, so it runs anywhere. As soon as a package pulls in cgo — the standard `net` or `os/user` packages in their default configuration, or any C dependency — the build links against the host's glibc dynamically and inherits the loader path. Check with `file` or `ldd`.
- Why is `gcompat` a shim rather than a real fix?It translates a subset of glibc behaviour onto musl; it is not glibc. Binaries relying on glibc symbol versioning, glibc-only extensions or NSS-backed lookups can still fail, and failures surface at run time in production rather than at build time. It is the right tool for a closed-source vendor binary and the wrong tool for code you control, which you should simply rebuild.
saying these in an interview costs you the question
- Claims the binary is corrupt or the wrong architecture
- Says Alpine's kernel is different from other Linux distributions
- Thinks chmod +x or PATH fixes the error
- Assumes musl is a drop-in binary-compatible replacement for glibc
- Believes any -static glibc build is fully self-contained