You know that moment when your computer is gaslighting you? When you run ls
inside a Docker container, see your executable sitting there with perfect permissions, but when you try to run it, the shell just shrugs and says “not found”?
Welcome to my Saturday morning.
The Setup: A Simple Go + TailwindCSS Build
I was working on a straightforward Dockerfile for a Go web app that uses TailwindCSS. Nothing fancy—just download the tailwindcss binary, make it executable, run it to process some CSS, and move on with life.
RUN curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 -o tailwindcss && \
chmod +x tailwindcss && \
./tailwindcss -i cmd/web/styles/input.css -o cmd/web/assets/css/output.css
The build kept failing with:
/bin/sh: ./tailwindcss: not found
But when I added ls -la tailwindcss
right before the execution, there it was:
-rwxr-xr-x 1 root root 120628068 Jul 12 13:59 tailwindcss
Perfect permissions. Right where it should be. Yet somehow, “not found.”
The Plot Twist: It’s Not About the File
After some detective work (and adding file tailwindcss
to the build), the mystery unraveled:
tailwindcss: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
for GNU/Linux 3.2.0, BuildID[sha1]=..., not stripped
Ah. There’s the smoking gun: dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2
.
The tailwindcss binary expects glibc (GNU C Library), but Alpine Linux—my base image of choice—uses musl libc. When the shell tries to execute the binary, it can’t find the dynamic linker it needs, so it just reports “not found” instead of something more helpful like “incompatible binary format” or “missing dynamic linker.”
Because why would error messages be intuitive?
The Fix: gcompat to the Rescue
The solution is surprisingly simple. Alpine provides a compatibility package called gcompat
that adds a glibc compatibility layer:
FROM golang:1.24-alpine AS build
RUN apk add --no-cache curl gcompat # <- The hero
That’s it. One additional package, and suddenly the tailwindcss binary can find its dynamic linker and run happily.
The Deeper Lesson: Docker Base Images Have Opinions
This whole adventure reminded me that Docker base images aren’t just “Linux with different package managers.” They make fundamental choices about system libraries that can bite you when you’re pulling in pre-compiled binaries from the wild.
Alpine’s decision to use musl instead of glibc makes perfect sense—musl is smaller, simpler, and more secure. But it also means that a huge chunk of pre-compiled Linux binaries won’t work out of the box.
Other options I considered:
- Switch to a glibc-based image like
golang:1.24
(Debian-based) - Find a musl-compiled version of tailwindcss
- Compile tailwindcss from source in the container
But gcompat
was the path of least resistance, and sometimes that’s exactly what you need when you just want to process some CSS and get on with your life.
The Takeaway
Next time Docker gaslight you with a “not found” error on a file that clearly exists, remember: it might not be about permissions or paths. It might be about fundamental incompatibilities hiding behind unhelpful error messages.
And maybe, just maybe, the solution is a single package install away.
Happy debugging!