Alpine's ~7 MB base image is why so many Dockerfiles start with FROM alpine:3.20. This guide writes a sane Alpine-based image, explains the musl-vs-glibc traps, and shows the patterns that keep image size honest.
Prerequisites
- Docker (or Podman) installed locally — see Docker + Compose on Ubuntu.
- A small app to package — we will use a Python Flask hello-world.
Step 1: A minimal Dockerfile
# syntax=docker/dockerfile:1.7
FROM alpine:3.20
RUN apk add --no-cache python3 py3-flask
WORKDIR /app
COPY app.py .
EXPOSE 5000
USER nobody
CMD ["python3", "app.py"]
app.py:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello from Alpine\n"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Build and run:
docker build -t skyline/hello-alpine .
docker run --rm -p 5000:5000 skyline/hello-alpine
curl http://localhost:5000/
Image size:
docker images skyline/hello-alpine
# Expect ~ 80 MB total (Alpine + Python).
Step 2: musl vs glibc — the one trap you must know
Alpine uses musl libc, not glibc. Most software is fine; pre-compiled glibc binaries (some commercial DBs, NodeJS native modules) will fail with cryptic errors. Two fixes:
- Build inside Alpine — set up the build chain in a multi-stage build (see Step 4).
- Use a glibc-based base —
debian:slimis ~30 MB more but accepts every binary.
Quick way to detect a glibc binary:
docker run --rm -v $(pwd):/check alpine sh -c 'apk add --no-cache file && file /check/your-binary'
If file reports interpreter /lib64/ld-linux-x86-64.so.2 you have glibc — Alpine has no such loader.
Step 3: --no-cache everywhere
RUN apk add --no-cache curl jq # no apk cache → smaller layer
For ephemeral build dependencies, use virtual packages:
RUN apk add --no-cache --virtual .build-deps gcc musl-dev make \
&& pip install --no-cache-dir uvicorn \
&& apk del .build-deps
That installs the build chain, builds the wheel, deletes the build chain — all in one layer, so the final image stays small.
Step 4: Multi-stage builds for compiled languages
# --- build stage ---
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags='-s -w' -o /out/app ./cmd/api
# --- runtime stage ---
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
COPY --from=build /out/app /usr/local/bin/app
USER nobody
ENTRYPOINT ["/usr/local/bin/app"]
Final image is ~12 MB even with Go's static runtime included.
Step 5: Useful Alpine quirks
/bin/shis ash, not bash. Either installbash(apk add bash) or write POSIX-compliant scripts.- No
useraddby default — useaddgroup+adduser(BusyBox flags):
RUN addgroup -S app && adduser -S -G app app
USER app
- timezone data is not installed by default. Add
tzdata:
RUN apk add --no-cache tzdata && \
cp /usr/share/zoneinfo/Asia/Riyadh /etc/localtime && \
echo "Asia/Riyadh" > /etc/timezone
- Health checks work normally:
HEALTHCHECK --interval=30s --timeout=5s CMD wget -qO- http://localhost:5000/ || exit 1
Step 6: Lock the base image
Pin the digest, not just the tag, for reproducible builds:
FROM alpine:3.20@sha256:0a4eaa0eecf5f8c050e5bba433f58c052be7587ee8af3e8b3910ef9ab5fbe9f5
Bump the digest deliberately, in a code review, not silently.
Verify
docker images skyline/hello-alpine --format 'table {{.Repository}}\t{{.Size}}'
docker run --rm skyline/hello-alpine id # confirm non-root user
docker scout cves skyline/hello-alpine # vulnerability scan
Conclusion
Alpine is a great default for stateless containers — small surface, fast pulls, decent security defaults. Reach for debian:slim instead the moment you need a glibc binary, native NodeJS modules, or libraries that bundle their own libc.
Next steps
- Install Alpine on a VM via 5-minute install.
- Manage packages via apk.
- For Docker on the host see Docker + Compose on Ubuntu.
Comments
0 total · 0 threads