$linuxjunkies
>

Alpine Linux on Servers and in Containers

Deploy Alpine Linux on servers and in containers: musl vs glibc trade-offs, apk package management, OpenRC init, security hardening, and container best practices.

IntermediateUbuntuDebianFedoraArch10 min readUpdated June 7, 2026

Before you start

  • Root or sudo access to an Alpine Linux system or a container runtime (Docker/Podman)
  • Basic familiarity with Linux shell and package management concepts
  • Network access to Alpine's CDN mirrors for package installation

Alpine Linux punches above its weight: a minimal base image under 8 MB, musl libc instead of glibc, OpenRC for init, and a security posture that treats attack-surface reduction as a first-class goal. It shows up everywhere—container base images, edge routers, firewalls, and lightweight VMs. Understanding its differences from mainstream distros keeps you from debugging mysterious compatibility problems at 2 AM.

musl vs glibc: What Actually Breaks and Why

The most common Alpine gotcha is musl libc. Most Linux software is developed and tested against the GNU C Library (glibc). musl is a clean-room, standards-compliant replacement that is smaller and simpler, but it diverges in important ways:

  • DNS resolver: musl does not support nsswitch.conf and uses a minimal stub resolver. It sends simultaneous A and AAAA queries; some poorly configured DNS servers rate-limit this and return SERVFAIL. The fix is usually adding options single-request to /etc/resolv.conf or adjusting your nameserver.
  • Thread-local storage and getentropy: Some pre-compiled glibc binaries simply will not run. Go binaries compiled with CGO_ENABLED=1 against glibc are a classic example.
  • Stack size defaults: musl defaults to a 128 KB thread stack; glibc defaults to 8 MB. Java, Rust async runtimes, and some Erlang/Elixir releases can crash or behave oddly without tuning (-Xss for JVM, RUST_MIN_STACK env var for Rust).
  • Locale support: musl has minimal built-in locale support. If your app uses setlocale() extensively, test carefully.

For fully static Go or Rust binaries built with CGO_ENABLED=0 or musl-gcc, Alpine is ideal—there is nothing to link against at runtime. For Python, Node.js, and Ruby, the apk packages are rebuilt against musl and work fine. For third-party proprietary binaries distributed as glibc ELFs (Datadog agent installers, some JDK builds), use the gcompat package or switch to a glibc-based image.

# Install glibc compatibility layer if you must run glibc binaries
apk add gcompat

The apk Package Manager

Alpine uses apk, which is fast, reproducible, and refreshingly explicit. It resolves dependencies but does not hide what it is doing.

Essential apk commands

# Update the index (equivalent to apt update)
apk update

# Install packages
apk add nginx curl bash

# Remove a package and its unused dependencies
apk del nginx

# Search for a package by name or description
apk search -v 'web server'

# Show package info
apk info -a nginx

# Upgrade all installed packages
apk upgrade

Pinning and virtual packages

Virtual packages let you group build-time dependencies under a single removable label—critical for keeping container images lean.

# Add build deps under a virtual name, build, then remove them all at once
apk add --virtual .build-deps gcc musl-dev python3-dev make
# ... your build commands ...
apk del .build-deps

Repositories

Alpine ships three repository tiers: main, community, and testing. Only main is enabled by default on server installs. Edit /etc/apk/repositories to enable community (most production servers need it).

cat /etc/apk/repositories
# Typical content (version will differ):
# https://dl-cdn.alpinelinux.org/alpine/v3.20/main
# https://dl-cdn.alpinelinux.org/alpine/v3.20/community
# Enable community by editing the file (uncomment or add the line)
echo 'https://dl-cdn.alpinelinux.org/alpine/v3.20/community' >> /etc/apk/repositories
apk update

Init System: OpenRC

Alpine uses OpenRC, not systemd. This is a deliberate choice—OpenRC is a dependency-based init that stays out of the way and has zero opinion about your dbus, journal, or network stack. On servers it works well; inside containers Alpine usually runs with no init at all (PID 1 is your process) or with tini.

Common OpenRC commands

# Start / stop / restart a service
rc-service nginx start
rc-service nginx stop
rc-service nginx restart

# Enable a service at boot (adds to default runlevel)
rc-update add nginx default

# Disable a service
rc-update del nginx default

# List all services and their state
rc-status

Runlevels

OpenRC uses named runlevels: sysinit, boot, default, nonetwork, and shutdown. For most services, default is where you register them. Service scripts live in /etc/init.d/; configuration overrides go in /etc/conf.d/.

# Write a minimal service script for a custom daemon
cat > /etc/init.d/myapp << 'EOF'
#!/sbin/openrc-run
description="My application daemon"
command="/usr/local/bin/myapp"
command_args="--config /etc/myapp/config.yaml"
command_background=true
pidfile="/run/myapp.pid"

depend() {
    need net
    after logger
}
EOF
chmod +x /etc/init.d/myapp
rc-update add myapp default

Security Posture

Alpine's security decisions are structural, not just cosmetic:

  • PaX/grsecurity-inspired patches: Alpine kernels historically shipped with hardening patches. Newer versions use mainline with CONFIG_HARDENED_USERCOPY, FORTIFY_SOURCE, and PIE-by-default compiler flags for all packages.
  • No setuid binaries by default: Run find / -perm -4000 on a fresh Alpine install and compare the output to Ubuntu—the difference is stark.
  • Minimal installed surface: No systemd-journald, no D-Bus, no NetworkManager, no Avahi, no cups. Fewer processes, fewer sockets, fewer exploitable paths.
  • busybox userland: Most traditional UNIX tools come from a single statically linked busybox binary. This is both a feature (tiny) and a caution (some flags differ from GNU coreutils; scripts that rely on GNU extensions may fail silently).

Firewall with nftables

Alpine ships with nftables available in community. For simple server rules:

apk add nftables
rc-update add nftables default

# Write a basic ruleset
cat > /etc/nftables.nft << 'EOF'
flush ruleset
table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;
        ct state established,related accept
        iif lo accept
        tcp dport { 22, 80, 443 } accept
        icmp type echo-request accept
    }
    chain forward { type filter hook forward priority 0; policy drop; }
    chain output  { type filter hook output  priority 0; policy accept; }
}
EOF
nft -f /etc/nftables.nft

Alpine in Containers

The official alpine Docker image is the de facto minimal base. A few patterns make it production-ready:

Multi-stage builds to avoid musl issues

# Build stage uses a full Go image; final stage is Alpine
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server

FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /app /app
USER nobody
ENTRYPOINT ["/app"]

Use tini as PID 1

Without a proper init, zombie processes accumulate and signals are not forwarded correctly to your application.

apk add --no-cache tini
# In Dockerfile:
# ENTRYPOINT ["/sbin/tini", "--", "/app"]

Verification

After a server setup, confirm the basics are in order:

# Check running services
rc-status

# Confirm firewall is active
nft list ruleset

# Verify musl version
musl-libc 2>/dev/null || /lib/ld-musl-*.so.1 2>&1 | head -1

# Check for unexpected setuid binaries
find / -xdev -perm -4000 -ls 2>/dev/null

Troubleshooting

  • DNS failures in containers: Add options single-request ndots:0 to /etc/resolv.conf or pass --dns-opt to Docker. This affects musl's resolver directly.
  • "not found" for a valid binary: Almost always a dynamically linked glibc binary. Run ldd /path/to/binary; if it lists libc.so.6 or libm.so.6, you need to rebuild against musl or use gcompat.
  • Service refuses to start after reboot: Check that you ran rc-update add <service> default, not just rc-service start. OpenRC does not persist ad-hoc starts across reboots.
  • Package not found: It may be in community or testing. Run apk search <name> after enabling the community repo. Avoid testing on production systems.
  • Java OOM or stack overflows: Pass -Xss4m (or higher) to the JVM to compensate for musl's default 128 KB thread stack.
tested on:Alpine 3.19Alpine 3.20

Frequently asked questions

Can I run Python or Node.js applications on Alpine without issues?
Yes. apk provides Python and Node.js packages rebuilt against musl. Pure-Python and pure-JS code runs without modification. C extensions compiled from source (pip install with a build step) also work fine because apk pulls in musl-dev and gcc. Pre-compiled binary wheels for glibc are the exception—pip may fall back to source builds, which requires adding build dependencies first.
Why does Alpine not use systemd?
Alpine's design philosophy prioritises minimalism and avoidance of large dependency chains. systemd pulls in dbus, udev, and other components that conflict with Alpine's goal of a sub-8-MB footprint. OpenRC fulfils the same init and service-supervision role with a much smaller binary and no mandatory runtime dependencies.
Is Alpine suitable as a desktop OS?
Technically yes—there are community-maintained Wayland and X11 setups—but it is not recommended for that use case. The musl quirks, minimal locale support, and absence of many desktop-oriented packages in main make the experience rough compared to Fedora or Ubuntu. Alpine is genuinely optimised for servers and containers.
How do I keep an Alpine server up to date securely?
Run apk update && apk upgrade regularly and subscribe to the Alpine security announcements mailing list at lists.alpinelinux.org. Alpine issues point releases (e.g. 3.20.x) that include security fixes without changing package APIs, so you can upgrade within a branch safely. Pin to a specific branch in /etc/apk/repositories rather than using the 'edge' channel on production systems.
What is the difference between Alpine 'edge' and a stable branch?
'edge' is Alpine's rolling-release channel, equivalent to Debian unstable. It gets the newest package versions but may introduce regressions. Stable branches (3.18, 3.19, 3.20…) receive only security and bug-fix updates and have a defined end-of-life date. Always use a numbered stable branch on servers and in production container images.

Related guides