Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Manage packages with APK on Alpine Linux

By updated

Improve this page

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

  1. Install, remove and update Alpine packages, and check the result after every action.
  2. Pick a supported Alpine branch and configure the repositories accordingly.
  3. Apply the container idioms --no-cache and --virtual .build-deps to slim down your images.
  4. Audit the integrity of installed files and repair an altered package.
  5. Build a package with abuild and publish a signed local repository.
  6. 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:

Terminal window
docker run --rm -it alpine:3.22@sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8 sh

Scope: 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 branchapk-tools versionReleaseEnd of support
v3.21apk-tools 2.142024-12-052026-11-01
v3.22apk-tools 2.142025-05-302027-05-01
v3.23apk-tools 3.02025-12-032027-11-01
v3.24apk-tools 3.02026-06-092028-06-01

Always check which generation you are dealing with before copying a command found online:

Terminal window
apk --version

Recorded 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:

Terminal window
cat /etc/alpine-release

The 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/main
https://dl-cdn.alpinelinux.org/alpine/v3.22/community

Prefix 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.

Terminal window
echo "https://repo.internal.example/alpine/v3.22/main" >> /etc/apk/repositories
wget -O /etc/apk/keys/internal-repo.rsa.pub https://repo.internal.example/internal-repo.rsa.pub
apk update

The 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.

Terminal window
apk update

The 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.

Terminal window
apk add htop curl

Check that the package is registered, without scanning the whole output:

Terminal window
apk info -e htop

The 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.

Terminal window
apk del htop

The 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.

Terminal window
apk update && apk upgrade

To 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:

Terminal window
apk upgrade --available

Search and inspect

apk search queries the repositories. Add -v to show the description next to each result, which avoids installing a package blindly.

Terminal window
apk search -v htop
htop-3.5.3-r0 - Interactive process viewer
htop-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.

Terminal window
apk info -W /usr/bin/htop
apk policy htop
/usr/bin/htop is owned by htop-3.5.3-r0
htop policy:
3.5.3-r0:
lib/apk/db/installed
https://dl-cdn.alpinelinux.org/alpine/v3.24/main

The 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.

Terminal window
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:

Terminal window
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/apk

2.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:

Terminal window
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/apk

The 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.

3.22
FROM alpine@sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8
RUN apk add --no-cache curl ca-certificates

The 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.

Terminal window
apk add --no-cache --virtual .build-deps gcc musl-dev python3-dev linux-headers

The group appears in /etc/apk/world as a timestamped virtual package, which proves APK treats it as a full-fledged entity:

.build-deps=20260712.082138
python3

Once the build is done, a single apk del cleans up:

Terminal window
apk del .build-deps

Check that the compiler is gone while the application still works:

Terminal window
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.

3.22
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-deps

The && 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:

Terminal window
apk verify openssh
openssh: -2 - No such file or directory

The correct use targets an archive, typically fetched with apk fetch before an offline installation:

Terminal window
apk fetch curl
apk verify curl-8.14.1-r3.apk
curl-8.14.1-r3.apk: 0 - OK

On an archive altered by even a single byte, the command fails with a non-zero exit code:

t.apk: -5 - IO ERROR

apk 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.

Terminal window
apk audit --system

On a healthy system, the command prints nothing and returns 0. Let us alter a binary to see it work:

Terminal window
printf zz >> /usr/bin/curl
apk audit --system
U usr/bin/curl

The 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:

Terminal window
apk audit --system --packages
curl-8.14.1-r3

Repair 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.

Terminal window
apk fix curl

The check afterwards must come back empty, which proves the system is back to its reference state:

Terminal window
apk audit --system

Without 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.

Terminal window
cat /etc/apk/world
alpine-baselayout
alpine-keys
alpine-release
apk-tools
busybox
curl
htop
musl-utils

It is a trivial text file, hence versionable in Git. A very handy use follows: rebuilding an identical system elsewhere.

Terminal window
cp /etc/apk/world packages.list

Then on the target machine:

Terminal window
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:

Terminal window
apk cache clean

On 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.

  1. Prepare the build environment. abuild refuses to run as root: you need a user member of the abuild group.

    Terminal window
    apk add alpine-sdk
    adduser -D builder
    addgroup builder abuild
  2. Generate the signing key and install it. The -a option stores the private key in the abuild configuration, -n avoids the interactive questions. The public key must be copied into /etc/apk/keys/ before the build, otherwise abuild fails when signing the index with UNTRUSTED signature.

    Terminal window
    su - builder -c 'abuild-keygen -a -n'
    cp /home/builder/.abuild/*.rsa.pub /etc/apk/keys/
  3. Write the APKBUILD file. It describes the package. The arch field is mandatory: without it, the build stops. Use noarch for a script independent of the processor.

    Terminal window
    pkgname=hello
    pkgver=1.0
    pkgrel=0
    pkgdesc="A test script"
    url="https://example.local"
    arch="noarch"
    license="MIT"
    source="hello.sh"
    package() {
    install -Dm755 "$srcdir/hello.sh" "$pkgdir/usr/bin/hello"
    }
  4. Compute the checksums, then build. abuild checksum is a mandatory prerequisite: abuild -r refuses 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
  5. Index and sign a hand-built repository. apk index builds the index, abuild-sign signs 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 *.apk
    abuild-sign APKINDEX.tar.gz

    The output confirms the signature: >>> Signed /srv/depot/x86_64/APKINDEX.tar.gz.

  6. 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 hello
    hello

    The output shows Bonjour depuis un paquet APK maison, and apk info -e hello confirms 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:

Terminal window
apk update && apk upgrade
apk audit --system --packages

The 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.

SymptomLikely causeSolution
ERROR: unable to select packagesIndex not refreshed, or package missing from the branchapk update, then apk search -v <pattern> to check the exact name
UNTRUSTED signatureRepository 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 branchapk policy <package> to see the versions really available
openssh: -2 - No such file or directory on apk verifyapk verify expects an .apk file, not a package nameUse apk audit --system for installed files
no such package on a local repository-X path pointing at the architecture folderPoint -X at the parent directory of x86_64
WARNING: opening from cache ... No such file or directoryIndex missing from the cache after an apk add --no-cacheHarmless, or apk update if the command needs the index
Abnormally large container imageapk add without --no-cache, or apk del in another RUNAdd --no-cache and group installation and removal in a single RUN

Key points

  • APK is a package manager, not a variant of apt: apk del also removes the dependencies that became orphans, and /etc/apk/world serves 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.18 image still downloads but has received no security fix since May 2025.
  • --no-cache on every apk add of a Dockerfile: it avoids 2.5 MB of index frozen in the layer and makes apk update unnecessary.
  • --virtual .build-deps then apk del .build-deps in the same RUN: 111 MB instead of 420 MB on a Python image, and no compiler left in production.
  • apk verify works on .apk files, never on an installed package. To check the files of a system, it is apk audit --system, and apk 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

6 questions
6 min
70% to pass

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

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn