Alpine Linux: apk, BusyBox, musl and the Docker image
What Alpine Linux is
Alpine Linux is a small Linux distribution built on three parts that differ from Debian, Ubuntu and Rocky Linux: the musl C library instead of glibc, BusyBox instead of the GNU tools, and its own package manager, apk. Its Docker image is 8.43 MB, which makes it a common base for containers; the official OpenCode image is one of them (OpenCode in Docker).
On a server or a virtual machine Alpine boots with OpenRC. In a container no init system runs, and this page is about containers.
Every command below was tested on 2026-10-26 in Docker 29.1.3 with alpine:latest, which was Alpine 3.24.3.
Alpine next to Debian, Ubuntu and Rocky
The same checks in four official images: alpine:latest, debian:13, ubuntu:24.04 and rockylinux/rockylinux:9.
| Alpine 3.24 | Debian 13 | Ubuntu 24.04 | Rocky 9.8 | |
|---|---|---|---|---|
| Image size | 8.43 MB | 120 MB | 78.2 MB | 236 MB |
| C library | musl | glibc 2.41 | glibc 2.39 | glibc 2.34 |
| /bin/sh | BusyBox ash | dash | dash | bash |
| bash | not installed | yes | yes | yes |
| ls, grep, find, date | BusyBox | GNU 9.7 | GNU 9.4 | GNU 8.32 |
| Packages | apk | apt-get | apt-get | dnf |
| New user | adduser | useradd | useradd | useradd, adduser |
| Download tool | wget | none | none | curl |
Image size as docker image ls shows it; GNU means GNU coreutils, and the wget of Alpine is part of BusyBox.
Each row is a habit that breaks when a script or a Dockerfile moves from one of the others to Alpine. The chapters below go through them.
Which Alpine is this
cat /etc/alpine-release
3.24.3
The file every distribution has gives the same version and the name:
cat /etc/os-release
NAME="Alpine Linux" ID=alpine VERSION_ID=3.24.3 PRETTY_NAME="Alpine Linux v3.24" HOME_URL="https://alpinelinux.org/" BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"
Packages: apk
| Command | What it does | In the test |
|---|---|---|
| apk update | downloads the package indexes to /var/cache/apk | two files, 2.9 MB |
| apk add --no-cache bash | installs a package; fetches the indexes for this one run and keeps nothing | the cache stayed at 4.0 KB |
| apk search -q 'php8*' | finds packages by name | php83, php84, php85 among others |
| apk info -e bash | prints the name when the package is installed | bash |
| apk del bash | removes a package | back to 16 packages |
| apk list --installed | lists the installed packages | 16 in the plain image |
Versions in names. Some packages carry their version in the name, so the PHP packages php83, php84 and php85 exist side by side. The command carries it too: php83 installs /usr/bin/php83 and no php, so a Dockerfile adds the link itself (next chapter).
Virtual packages. Build tools for one step, removed with one name afterwards:
apk add --no-cache --virtual .build-deps gcc musl-dev
apk del .build-deps
In the test the first command grew the system from 16 to 31 packages and 197.9 MiB; the second brought it back to 16 packages and 8208 KiB. In a Dockerfile both belong in one RUN: a later layer that deletes files does not make the earlier one smaller.
Alpine in a Dockerfile
vi Dockerfile
FROM alpine:3.24 RUN apk add --no-cache php83 php83-ctype php83-mbstring \ && ln -s /usr/bin/php83 /usr/local/bin/php RUN addgroup -g 1000 app && adduser -D -u 1000 -G app app USER app
- A pinned minor version. alpine:3.24 stays on 3.24; latest moves to the next release.
- --no-cache instead of apk update. The image keeps no package indexes: /var/cache/apk was empty in the built image.
- The php link for scripts that call php (versions in names).
- A user that is not root, see users and groups.
The image built in the test ran php -v as PHP 8.3.36 (cli) under UID 1000, and docker image ls showed 21.4 MB. More on images: Dockerfile.
BusyBox: where commands differ
ls, grep, find, date, sed, ps and wget are one program on Alpine, BusyBox, and it knows fewer options than the GNU tools. The same commands in debian:13 and alpine:latest:
| Command | Debian 13 (GNU) | Alpine (BusyBox) |
|---|---|---|
| echo a1 | grep -P "\d" | a1 | grep: unrecognized option: P, exit code 2 |
| date -d "+1 day" +%F | the date of tomorrow | date: invalid date '+1 day' |
| date -d "2026-10-26 12:00" +%A | Monday | Monday |
| find /etc -maxdepth 0 -printf "%f\n" | etc | find: unrecognized: -printf |
| ls --version | ls (GNU coreutils) 9.7 | ls: unrecognized option: version |
| sed -i "s/bash/sh/" h | edits the file | edits the file |
A missing option prints the usage of the BusyBox applet, so the error can scroll off the screen. The GNU tools are packages:
apk add --no-cache grep coreutils findutils
After it the three failing commands printed what Debian printed. The other way is to keep scripts to options both have: grep -E with [0-9] instead of grep -P with \d.
No bash: the script that is not found
A script that starts with #!/bin/bash fails on Alpine with a message that points at the wrong file:
./hello.sh
sh: ./hello.sh: not found
The script is there, and the exit code is 127. What is not found is /bin/bash from its first line. Two ways out:
- Install bash: apk add --no-cache bash In the test it added four packages, 10.0 MiB for the whole system.
- Write for sh: #!/bin/sh and POSIX syntax. /bin/sh on Alpine is the ash shell of BusyBox; on Debian and Ubuntu it is dash, so such a script runs on all of them.
musl: glibc programs do not run
Programs built on Debian, Ubuntu or Rocky Linux use glibc. On Alpine they fail with the same misleading message. Here /usr/bin/true, copied from debian:13:
./deb-true
sh: ./deb-true: not found
ldd shows why: the program asks for the glibc loader and for functions that musl does not have:
ldd ./deb-true
/lib64/ld-linux-x86-64.so.2 (0x733bac9c3000) libc.so.6 => /lib64/ld-linux-x86-64.so.2 (0x733bac9c3000) Error relocating ./deb-true: __vfprintf_chk: symbol not found Error relocating ./deb-true: __printf_chk: symbol not found Error relocating ./deb-true: __fprintf_chk: symbol not found
gcompat is a compatibility layer for such programs. With it, this small one ran with exit code 0:
apk add --no-cache gcompat
It is a layer, not glibc. For a real program, install its Alpine package or build it on Alpine.
Users and groups
useradd is not there: sh: useradd: not found. BusyBox has addgroup and adduser:
addgroup -g 1000 agent
adduser -D -u 1000 -G agent agent
id agent
uid=1000(agent) gid=1000(agent) groups=1000(agent),1000(agent)
-D creates the user without a password, so the command does not stop to ask for one; in a Dockerfile it has to. The home folder is /home/agent and the shell /bin/sh. More on users in general: Linux users.
Time zones
The image has no time zone data, and TZ is ignored without an error:
TZ=Europe/Helsinki date +%Z
UTC
apk add --no-cache tzdata
TZ=Europe/Helsinki date +%Z
EET
Checklist: moving to Alpine
- Scripts with #!/bin/bash: install bash or rewrite them for sh.
- GNU options such as grep -P, date -d "+1 day", find -printf: install grep, coreutils, findutils, or change the scripts.
- Programs built for glibc: use the Alpine package.
- useradd becomes adduser -D.
- tzdata when local time matters.
- In a Dockerfile: a pinned tag, apk add --no-cache, build tools in a virtual package removed in the same RUN.
Article author: Klavdiya Kondakova
| Linux | |
| Alpine Linux: apk, BusyBox, musl and the Docker image | |
| Debian | |
| Ubuntu | |
| Rocky Linux | |
| Docker | |
| Dockerfile | |
| OpenCode in Docker: auto mode on container/src only |