APK (Alpine Package Keeper) is the package manager of Alpine Linux.
You mostly meet it in container images, where every
megabyte counts. This page gives you the everyday commands
(add, del, update, upgrade, info, search), the two idioms that
make the difference in a Dockerfile (--no-cache and
--virtual .build-deps), the right way to check the integrity of an
Alpine system, and how to build a signed local repository. It is written
for administrators and developers who build images and
maintain minimalist servers.
What you will learn
- Install, remove and update Alpine packages, and check the result after every action.
- Pick a supported Alpine branch and configure the repositories accordingly.
- Apply the container idioms
--no-cacheand--virtual .build-depsto slim down your images. - Audit the integrity of installed files and repair an altered package.
- Build a package with
abuildand publish a signed local repository. - Tell apk-tools 2 and apk-tools 3 apart, whose behaviours differ.
Prerequisites and scope
You need an Alpine system, or more simply a disposable container, which is enough for everything that follows except the local repository part:
docker run --rm -it alpine:3.22@sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8 shScope: common Linux, outside the LFCS and RHCSA syllabus (these exams cover
apt and dnf). Level: L2 operational. Target capability:
build an Alpine image with no leftover cache or build tools, and
prove that the installed system has not been altered.
Alpine and APK in two minutes
Alpine Linux is a deliberately minimalist distribution. It replaces the glibc C library with the lighter musl libc, and gathers the usual Unix commands into a single binary, BusyBox. The result: a base image of about 8 MB, against several tens of megabytes for a Debian or an Ubuntu.
This choice has a price. Binaries compiled for glibc do not work as is on musl, which causes surprises with some proprietary software or compiled Python packages. Alpine excels at containers, embedded systems and single-purpose servers. It is not a desktop distribution.
APK is the tool that installs, removes and updates software. It
works with .apk archives containing the binaries, the metadata and
the dependencies, and it keeps the system state in a readable text
file, /etc/apk/world. Its speed is its signature: an
installation that takes several seconds with apt often completes in a
fraction of a second.
The two generations of apk-tools
This is the most important point to know today, and the one that traps
the most people. apk-tools has changed major version, and the two
generations coexist depending on the Alpine branch you use.
| Alpine branch | apk-tools version | Release | End of support |
|---|---|---|---|
| v3.21 | apk-tools 2.14 | 2024-12-05 | 2026-11-01 |
| v3.22 | apk-tools 2.14 | 2025-05-30 | 2027-05-01 |
| v3.23 | apk-tools 3.0 | 2025-12-03 | 2027-11-01 |
| v3.24 | apk-tools 3.0 | 2026-06-09 | 2028-06-01 |
Always check which generation you are dealing with before copying a command found online:
apk --versionRecorded on 30 September 2026: on Alpine 3.22.6, the output shows apk-tools 2.14.12, compiled for x86_64.;
on Alpine 3.24.1, it shows apk-tools 3.0.6-r0, compiled for x86_64..
The good news: every common command on this page works
identically on both generations. add, del, update, upgrade,
search, info, policy, audit, fix, verify and index keep the same
syntax. The differences concern rare options and the format of the
messages: apk-tools 3, for instance, drops the numeric code from errors
(openssh: No such file or directory instead of openssh: -2 - No such file or directory). One divergence deserves a warning: apk-tools 3 accepts
abbreviated options, which can make a typo look like a
valid option. Writing apk fix --check in the belief that you run a simulation actually executes
a real apk fix, because --check is read as an
abbreviation of --check-certificate.
Pick a supported Alpine branch
Each stable Alpine branch is supported for two years from its
release. After that date, it no longer receives any security fix,
and its repositories eventually disappear from the main mirror. It is a
classic trap: an alpine:3.18 image keeps downloading without error
although the branch has been end of life since May 2025.
Branches come in three channels:
- main: stable packages, maintained by the Alpine team, with security support.
- community: packages maintained by the community, with security support for the lifetime of the branch.
- testing: only available on
edge, with no guarantee at all.
Next to the numbered branches, edge is the development branch in
rolling release. It is always up to date but can break from one day to
the next. In production, stay on a stable branch with main and
community; keep edge for test environments.
To know the installed version:
cat /etc/alpine-releaseThe output shows for instance 3.22.5. If the first number matches a
branch whose end-of-support date has passed, plan the version
upgrade.
Configure the repositories
APK only knows the repositories listed in /etc/apk/repositories, a
plain text file, one URL per line. It is the central point of the
configuration, and there is nothing else to know.
On an Alpine 3.22 image, it contains exactly this:
https://dl-cdn.alpinelinux.org/alpine/v3.22/mainhttps://dl-cdn.alpinelinux.org/alpine/v3.22/communityPrefix a line with # to disable a repository without deleting it. To
change branch, replace the version number in both URLs, then
apply the switch with apk upgrade --available (detailed below). Any
change to this file requires an apk update to be taken into account.
Add a third-party repository and its key
APK refuses by default any package not signed by a key it knows. The
trusted public keys live in /etc/apk/keys/. Adding an internal
repository therefore takes two steps: declare the URL, then install the matching
public key.
echo "https://repo.internal.example/alpine/v3.22/main" >> /etc/apk/repositorieswget -O /etc/apk/keys/internal-repo.rsa.pub https://repo.internal.example/internal-repo.rsa.pubapk updateThe output of apk update must list your repository without a warning. If you
see UNTRUSTED signature, the public key is missing or does not match
the one that signed the index.
The everyday commands
These six commands cover almost every need. Each one is shown
with its verification command, because an installation that prints OK
does not yet prove that the expected binary is in place.
Refresh the repository index
apk update downloads the package metadata from the declared
URLs. It is the prerequisite for any installation, and the only way to see
a recently published package appear.
apk updateThe output ends with a line such as OK: 26325 distinct packages available, which confirms the number of packages seen in the repositories.
Install a package
apk add installs one or more packages and resolves the
dependencies automatically.
apk add htop curlCheck that the package is registered, without scanning the whole output:
apk info -e htopThe command prints htop if it is installed and returns nothing otherwise. Its
exit code is 0 or 1 accordingly, which makes it usable in a
script.
Remove a package
apk del removes the package and the dependencies that became orphans. It is a
more aggressive behaviour than apt remove, and it is deliberate.
apk del htopThe output details the cascading removals:
(1/4) Purging htop (3.5.3-r0)(2/4) Purging lsof (4.99.6-r0)(3/4) Purging libncursesw (6.6_p20260516-r0)(4/4) Purging ncurses-terminfo-base (6.6_p20260516-r0)The libraries pulled only by htop go with it. On the other hand,
the configuration files you edited by hand are kept.
Update the system
apk upgrade brings every package to the latest version available in the
configured branch. It does not move you from Alpine 3.22 to 3.24: for that,
you must first edit /etc/apk/repositories.
apk update && apk upgradeTo force the switch after changing the branch in the repositories file,
add --available (short form -a), which reinstalls the packages whose
version differs even if the number looks equivalent:
apk upgrade --availableSearch and inspect
apk search queries the repositories. Add -v to show the description
next to each result, which avoids installing a package blindly.
apk search -v htophtop-3.5.3-r0 - Interactive process viewerhtop-doc-3.5.3-r0 - Interactive process viewer (documentation)apk info details a package. Without an option, it only shows the description;
-a gives the full view (version, web page, licence, dependencies,
files). Two variants help every day: apk info -W answers
the question "which package owns this file", and apk policy shows which
repository provides which version.
apk info -W /usr/bin/htopapk policy htop/usr/bin/htop is owned by htop-3.5.3-r0htop policy: 3.5.3-r0: lib/apk/db/installed https://dl-cdn.alpinelinux.org/alpine/v3.24/mainThe lib/apk/db/installed line means this version is the one
currently installed. apk policy is the diagnostic tool to pull out when
a package does not update as expected: it immediately reveals whether a
third-party repository provides a competing version.
Pin an exact version
You can request an exact version with the package=version syntax. It is
the basis of a reproducible image: without pinning, two builds a
few weeks apart do not ship the same binaries.
apk add "htop=3.5.3-r0"The -r0 suffix is the Alpine revision number of the package: it changes
when Alpine rebuilds the software without its upstream version changing. It is
an integral part of the identifier and cannot be omitted.
Beware of the tight coupling with the branch. A version present in Alpine 3.22 does not necessarily exist in 3.24, and the command then fails unambiguously:
ERROR: unable to select packages: breaks: world[htop=3.4.1-r0]It is the main criticism of pinning on Alpine: it breaks at every branch upgrade. The compromise I keep is to pin the base image by its digest and let the packages follow the branch, as described in the next section.
The two container idioms that change everything
If you were to remember only two things about APK, it would be these. They
do not exist in apt or dnf in this form, and they explain
why Alpine images are so compact.
--no-cache: leave nothing behind
By default, apk writes the downloaded indexes to /var/cache/apk/. In
a container, these files are useless after the build, but they are
frozen in the image layer. The finding is clear:
docker run --rm alpine:3.22@sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8 sh -c "apk add curl >/dev/null 2>&1; du -sh /var/cache/apk"2.5M /var/cache/apk2.5 MB of waste on a base image that weighs 8. With --no-cache,
APK downloads the index, uses it in memory and never writes it to
disk:
docker run --rm alpine:3.22@sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8 sh -c "apk add --no-cache curl >/dev/null 2>&1; du -sh /var/cache/apk"4.0K /var/cache/apkThe directory is empty. It is also why apk update
becomes superfluous in a Dockerfile: --no-cache refreshes the index at
every invocation. The old formula apk update && apk add ... && rm -rf /var/cache/apk/* still found everywhere is obsolete and is replaced
by a single line.
FROM alpine@sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8
RUN apk add --no-cache curl ca-certificatesThe base image is pinned here by its sha256 digest rather than by
its tag: a tag such as alpine:3.22 is reassigned at every fix,
the digest is not.
--virtual: install to build, then remove everything
Many Python, Node or Ruby packages must be compiled at
installation, which requires a compiler and development headers.
Keeping gcc in the final image means carrying hundreds of useless
megabytes and widening the attack surface for nothing.
The --virtual option groups several packages under a collective name, which
you then remove in a single move. The convention is to call it
.build-deps, the leading dot having no particular meaning for
APK.
apk add --no-cache --virtual .build-deps gcc musl-dev python3-dev linux-headersThe group appears in /etc/apk/world as a timestamped virtual package,
which proves APK treats it as a full-fledged entity:
.build-deps=20260712.082138python3Once the build is done, a single apk del cleans up:
apk del .build-depsCheck that the compiler is gone while the application still works:
command -v gcc || echo "gcc absent"python3 -c "import psutil; print(psutil.__version__)"The critical point is that everything must fit in a single RUN instruction.
A Docker layer is immutable: if you install gcc in one RUN and
remove it in the next, the intermediate layer still holds the
hundreds of megabytes of the compiler, and the final image does not lose a single
byte.
FROM alpine@sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8
RUN apk add --no-cache python3 py3-pip && \ apk add --no-cache --virtual .build-deps gcc musl-dev python3-dev linux-headers && \ pip install --no-cache-dir --break-system-packages "psutil==7.0.0" && \ apk del .build-depsThe && is not cosmetic: it chains the commands in the same
layer. It is the only reason the trick works. The gain is
massive: this image weighs 111 MB, against 420 MB for the same recipe
without the --virtual and the apk del, that is 309 MB of compiler and
headers that never go to production.
Note in passing linux-headers among the build dependencies. It is the
kind of package you discover by reading pip's error message: without
it, the build of psutil stops on fatal error: linux/ethtool.h: No such file or directory. Build dependencies are specific to each
library, and there is no universal list: start from
gcc musl-dev, add the headers the error message asks for, and
do not forget to include them in the .build-deps group so that they leave
with it.
To go further on building lightweight images, the page Optimise container image size (in French) covers multi-stage builds and the choice of the base image.
Check integrity and audit the system
This is the section where mistakes are most frequent, because two
commands have deceptively similar names. apk verify and apk audit
do not do the same job.
apk verify checks .apk files, not installed packages
apk verify takes .apk files as arguments and validates their checksum
and their cryptographic signature. Passing it the name of an
installed package produces an error, which confuses many people:
apk verify opensshopenssh: -2 - No such file or directoryThe correct use targets an archive, typically fetched with apk fetch
before an offline installation:
apk fetch curlapk verify curl-8.14.1-r3.apkcurl-8.14.1-r3.apk: 0 - OKOn an archive altered by even a single byte, the command fails with a non-zero exit code:
t.apk: -5 - IO ERRORapk audit compares installed files with the package database
To answer the real question, "have the files of my system been
modified since their installation", the tool is apk audit. Without an option,
it only inspects the configuration files (the protected paths, under
/etc for the most part). The --system option extends the check to every
other file, binaries and libraries included.
apk audit --systemOn a healthy system, the command prints nothing and returns 0. Let us alter a
binary to see it work:
printf zz >> /usr/bin/curlapk audit --systemU usr/bin/curlThe U prefix signals a modified file; an A would signal a file
added outside any package. The --packages option goes straight to the
faulty package, which is exactly what you need to act:
apk audit --system --packagescurl-8.14.1-r3Repair with apk fix
apk fix reinstalls the given packages and restores the original files.
It is the logical follow-up of an audit that found something.
apk fix curlThe check afterwards must come back empty, which proves the system is back to its reference state:
apk audit --systemWithout an argument, apk fix tackles every package and the consistency of the
world file. Note that it does not detect on its own an altered binary: it is
apk audit that finds, apk fix that repairs. Chaining the two is the
pair to remember.
/etc/apk/world, the declared state of the system
/etc/apk/world is the list of packages you explicitly
requested. Dependencies pulled automatically do not appear there. apk add
adds a line, apk del removes it, and APK considers this file the
reference truth about what the system must contain.
cat /etc/apk/worldalpine-baselayoutalpine-keysalpine-releaseapk-toolsbusyboxcurlhtopmusl-utilsIt is a trivial text file, hence versionable in Git. A very handy use follows: rebuilding an identical system elsewhere.
cp /etc/apk/world packages.listThen on the target machine:
apk add $(cat packages.list)The output ends with a count such as OK: 14.0 MiB in 29 packages,
which you just compare with the one of the source machine. You can also
edit world by hand, but the changes only take effect after an
apk fix, which reconciles the real state with the declared state.
The APK cache on an installed system
Contrary to what you often read, there is no --cache option.
With apk-tools 2, trying it returns apk: option is ambiguous: cache. With
apk-tools 3, it is more treacherous: abbreviated options are accepted, and
apk add --cache htop really does install htop, without the slightest
warning (recorded on Alpine 3.24.1). The cache is a
directory you enable by creating the /etc/apk/cache symbolic link,
which the Alpine installer does on a real machine and the
Docker image does not.
Where the cache exists, the downloaded .apk archives are kept there, which
speeds up reinstallations and lets you work offline. Cleaning up
is done with:
apk cache cleanOn a system without a configured cache, the command does nothing and returns 0,
without error. Check the space recovered with du -sh /var/cache/apk. In a
container, the question does not arise: --no-cache has already settled everything.
Build a package and a signed local repository
Distributing an internal binary or a home-made script as a real package brings
version management, clean uninstallation and signature
verification. The tool is abuild, provided by the
alpine-sdk meta-package.
-
Prepare the build environment.
abuildrefuses to run asroot: you need a user member of theabuildgroup.Terminal window apk add alpine-sdkadduser -D builderaddgroup builder abuild -
Generate the signing key and install it. The
-aoption stores the private key in theabuildconfiguration,-navoids the interactive questions. The public key must be copied into/etc/apk/keys/before the build, otherwiseabuildfails when signing the index withUNTRUSTED signature.Terminal window su - builder -c 'abuild-keygen -a -n'cp /home/builder/.abuild/*.rsa.pub /etc/apk/keys/ -
Write the
APKBUILDfile. It describes the package. Thearchfield is mandatory: without it, the build stops. Usenoarchfor a script independent of the processor.Terminal window pkgname=hellopkgver=1.0pkgrel=0pkgdesc="A test script"url="https://example.local"arch="noarch"license="MIT"source="hello.sh"package() {install -Dm755 "$srcdir/hello.sh" "$pkgdir/usr/bin/hello"} -
Compute the checksums, then build.
abuild checksumis a mandatory prerequisite:abuild -rrefuses to start if the sources have no checksums.Terminal window su - builder -c 'cd ~/hello && abuild checksum && abuild -r'The package lands in
~/packages/<repository>/<arch>/, with an already signed index:/home/builder/packages/builder/x86_64/APKINDEX.tar.gz/home/builder/packages/builder/x86_64/hello-1.0-r0.apk -
Index and sign a hand-built repository.
apk indexbuilds the index,abuild-signsigns it. The latter requires the name of the index file: run alone, it just prints its help and exits with an error.Terminal window apk index --rewrite-arch x86_64 -o APKINDEX.tar.gz *.apkabuild-sign APKINDEX.tar.gzThe output confirms the signature:
>>> Signed /srv/depot/x86_64/APKINDEX.tar.gz. -
Install from the local repository. The path given to APK is that of the parent directory of the architecture folder, not the folder itself: APK adds
/x86_64/on its own.Terminal window apk add -X /srv/depot hellohelloThe output shows
Bonjour depuis un paquet APK maison, andapk info -e helloconfirms the registration.
Two details explain the majority of failures on this procedure. First
the order: the public key must be in place before abuild -r, because the
build ends with an index signature that APK refuses if it does not know
the key. Then the repository path: pointing -X directly at the
x86_64 folder gives a confusing no such package, although the package is there.
The --rewrite-arch option, for its part, reconciles a noarch package
dropped into a concrete architecture directory.
To make the repository permanent, add its path or URL to
/etc/apk/repositories, like any remote repository.
Security and points of attention
APK's security rests entirely on the signature chain. Each
repository index is signed, each trusted key lives in /etc/apk/keys/, and
a package whose signature does not match is rejected. This mechanism only
protects you if you do not bypass it.
The three mistakes I see most often fit in a few words.
Using --allow-untrusted to "push through" an installation that complains
removes the only barrier against a forged package. Staying on an
end-of-life branch deprives the system of security fixes while giving
the illusion that all is well, since apk upgrade keeps answering OK.
Finally, leaving gcc and the development headers in a production
image gives an attacker the means to build their own tools from
inside the container.
The routine check fits in two commands, to add to a scheduled task or an image scan:
apk update && apk upgradeapk audit --system --packagesThe first applies the fixes, the second proves that the installed files still match the original packages. An empty output on the second is the expected result.
Troubleshooting
Here are the symptoms met most often, with their real cause. The "solution" column gives the command to run, not a vague lead.
| Symptom | Likely cause | Solution |
|---|---|---|
ERROR: unable to select packages | Index not refreshed, or package missing from the branch | apk update, then apk search -v <pattern> to check the exact name |
UNTRUSTED signature | Repository public key missing from /etc/apk/keys/ | Install the repository's public key, then rerun apk update |
breaks: world[package=version] | Pinned version missing from the configured branch | apk policy <package> to see the versions really available |
openssh: -2 - No such file or directory on apk verify | apk verify expects an .apk file, not a package name | Use apk audit --system for installed files |
no such package on a local repository | -X path pointing at the architecture folder | Point -X at the parent directory of x86_64 |
WARNING: opening from cache ... No such file or directory | Index missing from the cache after an apk add --no-cache | Harmless, or apk update if the command needs the index |
| Abnormally large container image | apk add without --no-cache, or apk del in another RUN | Add --no-cache and group installation and removal in a single RUN |
Key points
- APK is a package manager, not a variant of
apt:apk delalso removes the dependencies that became orphans, and/etc/apk/worldserves as the reference of the declared system state. - Two generations coexist: apk-tools 2 on Alpine 3.22 and earlier, apk-tools 3 from Alpine 3.23 onwards. The common commands are identical; check with
apk --version. - An Alpine branch is supported for two years. An
alpine:3.18image still downloads but has received no security fix since May 2025. --no-cacheon everyapk addof aDockerfile: it avoids 2.5 MB of index frozen in the layer and makesapk updateunnecessary.--virtual .build-depsthenapk del .build-depsin the sameRUN: 111 MB instead of 420 MB on a Python image, and no compiler left in production.apk verifyworks on.apkfiles, never on an installed package. To check the files of a system, it isapk audit --system, andapk fix <package>to repair.- The signature is not optional: install the public key in
/etc/apk/keys/rather than resorting to--allow-untrusted. - A local repository is declared by its parent directory:
apk add -X /srv/depot, never-X /srv/depot/x86_64.
Knowledge check
Check that the essentials of this guide are acquired. The questions only cover what has just been explained here.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Manage packages with Pacman
A new full quiz with random questions
Work again only on the questions you missed