Alpine Linux for Docker: Building Minimal, Secure Container Images
Why Alpine Linux Dominates Container Base Images
Alpine Linux is a ~5MB Linux distribution built around musl libc and BusyBox. For containers, this matters: smaller base images mean faster pulls, less surface area for vulnerabilities, and lower attack surface in production.
Compare a typical base image size:
ubuntu:24.04— ~77MBdebian:bookworm-slim— ~75MBalpine:3.20— ~7MB
The size difference becomes significant when you're running hundreds of containers or scanning every layer for CVEs.
Understanding Alpine's musl libc
The main compatibility consideration with Alpine is that it uses musl libc instead of glibc (GNU C Library). Most software written for Linux assumes glibc. Issues arise when:
- A binary is dynamically linked to glibc — it will crash on Alpine
- A language runtime (Python, Node.js, Ruby) has a C extension compiled against glibc
- Software uses glibc-specific features (
getaddrinfowith specific flags,dlopenbehavior, etc.)
# Check what libraries a binary needs
ldd /usr/bin/myapp
# If it shows /lib/x86_64-linux-gnu/libc.so.6, it needs glibc
# Alpine provides /lib/ld-musl-x86_64.so.1
# Test a binary on Alpine
docker run --rm -v $(pwd)/myapp:/myapp alpine:3.20 /myapp
# If it fails: "not found" or "exec format error" — glibc dependency
For most interpreted languages (Python, Node.js, Ruby) and compiled-from-source Go binaries, Alpine works without issues. For pre-compiled glibc binaries, use a glibc-based image instead.
Basic Dockerfile with Alpine
FROM alpine:3.20
# apk is Alpine's package manager
RUN apk add --no-cache nginx
# Alpine uses /etc/nginx, same structure as other distros
COPY nginx.conf /etc/nginx/nginx.conf
COPY html/ /var/www/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Key flags:
--no-cache— don't cache the index in the image layer (reduces size by ~5MB)- Without
--no-cache, Alpine creates/var/cache/apk/in the layer
docker build -t myapp:latest .
docker images myapp:latest
# REPOSITORY TAG IMAGE ID SIZE
# myapp latest abc123 12.5MB ← nginx on Alpine
Multi-Stage Builds: The Right Way to Use Alpine
The real power of Alpine is in multi-stage builds — use a full build environment to compile your application, then copy only the artifact to a minimal Alpine runtime image.
Go Application
Go binaries can be compiled statically (no external dependencies), making them ideal for Alpine or even scratch:
# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /myapp ./cmd/server
# Stage 2: Runtime
FROM alpine:3.20
# ca-certificates is needed for HTTPS calls
RUN apk add --no-cache ca-certificates tzdata
# Run as non-root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY /myapp .
USER appuser
EXPOSE 8080
CMD ["/app/myapp"]
Final image size: ~15MB (7MB Alpine + ~8MB Go binary). Compare to golang:1.22 base: ~800MB.
Node.js Application
Node.js on Alpine requires node:alpine tag (or node:lts-alpine):
# Stage 1: Dependencies
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Stage 2: Build (if needed, e.g., TypeScript)
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 3: Runtime
FROM node:22-alpine AS runtime
ENV NODE_ENV=production
WORKDIR /app
RUN addgroup -S nodejs && adduser -S nextjs -G nodejs
COPY /app/node_modules ./node_modules
COPY /app/dist ./dist
COPY package.json .
USER nextjs
EXPOSE 3000
CMD ["node", "dist/index.js"]
Python Application
FROM python:3.12-alpine AS builder
WORKDIR /app
COPY requirements.txt .
# Install build deps for packages with C extensions (e.g., cryptography, psycopg2)
RUN apk add --no-cache --virtual .build-deps \
gcc musl-dev libffi-dev openssl-dev postgresql-dev && \
pip install --prefix=/install -r requirements.txt && \
apk del .build-deps
FROM python:3.12-alpine
WORKDIR /app
COPY /install /usr/local
# Runtime library deps (not build deps)
RUN apk add --no-cache libpq
COPY . .
RUN adduser -D appuser
USER appuser
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
The --virtual .build-deps pattern installs build tools, compiles packages, then removes the build tools — keeping the image layer clean.
Common apk Operations in Dockerfiles
# Install multiple packages (one RUN to minimize layers)
RUN apk add --no-cache curl bash git openssh-client
# Install a specific version
RUN apk add --no-cache nginx=1.26.2-r0
# Add a testing or edge repository
RUN apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main \
some-package
# List available package versions
apk list nginx
# Search for a package
apk search nginx
# Show package info and dependencies
apk info -d nginx
Security Best Practices for Alpine Containers
1. Run as Non-Root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
2. Read-Only Filesystem
docker run --read-only \
--tmpfs /tmp \
--tmpfs /var/run \
myapp:latest
Or in docker-compose:
services:
app:
image: myapp:latest
read_only: true
tmpfs:
- /tmp
- /var/run
3. No New Privileges
docker run --security-opt=no-new-privileges myapp:latest
4. Pin Image Versions
# Bad: pulls whatever is current
FROM alpine:latest
# Good: pin to a specific version
FROM alpine:3.20.3
# Better: pin to a digest (immutable)
FROM alpine@sha256:a8560b36e8b8210634f77d9a1296573b08b9a20a0a0e...
5. Scan for Vulnerabilities
# Trivy (recommended)
docker run --rm aquasec/trivy image myapp:latest
# Grype
docker run --rm anchore/grype myapp:latest
# Check Alpine CVE status
# https://security.alpinelinux.org/
When NOT to Use Alpine
Alpine is not always the right choice:
- glibc-dependent binaries: Use
debian:slimorubuntuinstead - Java applications: JVM expects glibc; use
eclipse-temurin:21-jre(glibc-based) - Debugging in production: Alpine's minimal tooling makes debugging harder — consider
distrolessor a debug variant with tools added - Complex DNS-dependent apps: musl's
getaddrinfodoesn't supportndotsthe same way glibc does, causing DNS resolution differences in Kubernetes
For Java specifically:
# Use official JRE image (glibc-based) not Alpine
FROM eclipse-temurin:21-jre-jammy
Minimal Dockerfile Checklist
| Practice | Why |
|---|---|
apk add --no-cache | Removes cache from layer |
| Multi-stage build | Keeps build tools out of final image |
Single RUN for apk installs | Fewer layers |
| Non-root user | Reduces blast radius if compromised |
--no-new-privileges | Prevents privilege escalation |
| Pin image versions | Reproducible builds |
| Remove dev packages after build | Only runtime deps in final image |
Alpine's combination of small size, active maintenance, and straightforward apk tooling makes it the default choice for container base images when glibc compatibility isn't a concern.
Was this article helpful?
Linux Systems Engineer
Everything runs on Linux — I make sure it runs well. From kernel tuning to systemd debugging, I live in the terminal. If your server is misbehaving, I've probably seen that exact dmesg output before.
Related Articles
Alpine Linux apk Reference: Package Management, Repositories, and System Administration
A complete reference for Alpine Linux's apk package manager — adding packages, managing repositories, pinning versions, building custom packages, and running Alpine as a minimal server OS.
Rocky Linux 9 Server Hardening: Security Baseline from First Boot
A production security baseline for Rocky Linux 9 — covering SELinux configuration, firewalld rules, SSH hardening, auditd, AIDE integrity checking, and CIS Benchmark compliance checks.
Ubuntu Zero-Downtime Patching: Unattended Upgrades, Livepatch, and USN Tracking
How to keep Ubuntu servers patched automatically without unplanned reboots — combining unattended-upgrades, Canonical Livepatch, and USN monitoring into a practical patching strategy.
Systemd Service Hardening: Sandbox Your Daemons
How to use systemd's built-in sandboxing directives to isolate services, restrict filesystem access, and harden daemons without containers.
Linux User & Group Management: From Root to Least Privilege
Create and manage Linux users and groups, configure sudo access, understand /etc/passwd and /etc/shadow, and implement PAM authentication policies.
JFrog Artifactory: Setup, Repository Configuration, and Pipeline Integration
How to set up JFrog Artifactory, configure local and remote repositories, integrate with Docker and build tools, use Artifactory query language for artifact searches, and set up release bundles.
More in Linux
View all →Linux Control Groups (cgroups V2): Limiting CPU And Memory For Production Workloads
If you're running production workloads without cgroup constraints, you're one runaway process away from a bad day. Here's everything you need to configure...
openSUSE MicroOS and Transactional Updates: Atomic System Updates with Btrfs Snapshots
How openSUSE's transactional-update and Btrfs snapshot integration enables atomic, rollback-safe system updates — and how MicroOS takes this to a fully immutable, container-optimized OS.
openSUSE and SUSE Linux Enterprise: YaST Administration and zypper Package Management
A practical guide to SUSE Linux administration — using YaST for system configuration, zypper for package management, managing repositories, and understanding the differences between openSUSE Leap, Tumbleweed, and SUSE Linux Enterprise.
RHEL 9 Performance Tuning: tuned, sysctl, and CPU/Memory Optimization
How to tune RHEL 9 performance using tuned profiles, sysctl parameters, CPU governors, NUMA topology, and memory settings — for database servers, web workloads, and latency-sensitive applications.