Building / consuming alpine Linux packages inside containers and images
How to build alpine Linux packages you can later install inside other alpine based containers or images
What we will build ?
The best way to build software under alpine Linux is to write an
APKBUILD
file and rely on the effective
distribution tools to create a package for
us.
We will create in this tutorial a package inside an alpine container and see how we can consume it inside another container, and for building an image. We will use for that :
the alpine Linux official image and official packages,
and a manifest we will create from scratch to build a package registry service programmed in rust: reposerve
Build environment
We start by creating a tmp
directory in the current working directory
mkdir tmp
Let’s run an alpine container in interactive mode to build our package. The tmp
host directory is
mapped to the packager
user home directory.
podman run --rm --interactive --tty \
--volume "$(pwd)/tmp:/home/packager" \
alpine:3.17 sh
Inside the container lets prepare the abuild
environment
# install essentials apk package
apk add sudo build-base alpine-sdk
# create a packager user and add him to sudo list
adduser -D packager
addgroup packager abuild
echo 'packager ALL=(ALL) NOPASSWD:ALL' \
>/etc/sudoers.d/packager
# then open an sh as packager user
sudo -u packager sh
We will now generate a pair of keys. Follow this step only for the first package as we can reuse the same keys for all other packages we need to build.
abuild-keygen -n --append --install
Create the package
We now need something to build, so let’s quickly create a
APKBUILD
file to build reposerve
:
cd
mkdir -p reposerve/main/reposerve
/home/packager/reposerve/main/reposerve/APKBUILD
# Maintainer: Eric BURGHARD <eric@itsufficient.me>
pkgname=reposerve
pkgver=0.5.0
pkgrel=0
pkgdesc="Simple alpine packages server for consuming and updating packages in CI/CD"
url="https://git.itsufficient.me/rust/reposerve"
arch="x86_64"
license="ASL 2.0"
makedepends="cargo clang-dev llvm-dev"
depends="abuild"
source="$pkgname-$pkgver.tar.gz::https://github.com/eburghar/reposerve/archive/refs/tags/$pkgver.tar.gz"
build() {
cargo build --release
}
check() {
cargo test all --release
}
package() {
install -Dm755 target/release/"$pkgname" \
"$pkgdir"/usr/bin/"$pkgname"
}
sha512sums="
d20c5fb5d9f769add480e4897f63565d8240005df9e7fa316a22a380c2eeac258e41476ec6936d0af4ea51e6f0639316448ba06c586ab08b3c321601fe3abb87 reposerve-0.5.0.tar.gz
"
The directory structure is important because abuild
expects that the parent of the directory
containing the APKBUILD
corresponds to the name of a package repository (main).
aports project contains all the packages definitions for a
given alpine version, but I prefer building alpine packages independently of one another.
Just run :
cd /home/packager/reposerve/main/reposerve
# REPODEST is set to add an additional level
# in package directories to represent alpine version
REPODEST=~/packages/3.17 abuild -r
and after a few minutes you should have the reposerve
binary packaged in
/home/packager/packages/3.17/main/x86_64/reposerve-0.5.0-r0.apk
Install the package
Exit from the previous container and rerun the same podman
command to fire another one :
podman run --rm --interactive --tty \
--volume "$(pwd)/tmp:/home/packager" \
alpine:3.17 sh
Copy the public key created in previous step in the apk
keys’ directory and install the package
locally :
cp /home/packager/.abuild/*.rsa.pub \
/etc/apk/keys/
apk add /home/packager/packages/3.17/main/x86_64/reposerve-0.5.0-r0.apk
You can verify that reposerve
has been installed as expected by running :
reposerve --help
reposerve 0.5.0
Usage: reposerve [-c <config>] [-d] [-v] [-a <addr>]
Simple alpine Linux packages server
Options:
-c, --config configuration file (/etc/reposerve.yaml)
-d, --dev dev mode: enable /webhook and /upload without jwt (false)
-v, --verbose more detailed output (false)
-a, --addr addr:port to bind to (0.0.0.0:8080)
--help display usage information
Build an image
Let’s create a Containerfile
inside the tmp
directory which should now contains an .abuild
directory as well a packages
directory that contains the package built on the previous step.
tmp/Containerfile
FROM alpine:3.17
# you should change this line to match the
# keys' filenames generated on previous step
COPY .abuild/packager-622b138b.rsa.pub \
.abuild/packager-622b138b.rsa \
/etc/apk/keys/
COPY packages/3.17/main/x86_64/reposerve-0.5.0-r0.apk \
/tmp/
RUN apk add alpine-sdk tini \
/tmp/reposerve-0.5.0-r0.apk
ENTRYPOINT ["tini", "--", "/usr/bin/reposerve"]
Build the image with buildha or any other tool :
buildah bud --tag reposerve:0.5.0 tmp
And finally test your image :
podman run --rm reposerve:0.5.0 --help
Serving packages
Create a reposerve
configuration file in the tmp
directory :
tmp/reposerve.yaml
dir: /home/packager/packages
Run the service in dev mode :
podman run \
--rm \
--publish 8080:8080/tcp \
--volume "$(pwd)/tmp:/home/packager" \
reposerve:0.5.0 \
--config /home/packager/reposerve.yaml \
--dev
Your packages are now available at http://localhost:8080
It is easy to consume the packages inside a container. Without stopping the reposerve container, fire a new alpine container
podman run --rm --interactive --tty \
--volume "$(pwd)/tmp:/home/packager" \
alpine:3.17 sh
and do
# copy the public key
cp /home/packager/.abuild/*.rsa.pub \
/etc/apk/keys/
# add the repository (the ip address is the container's default gateway)
sed --in-place '1ihttp://10.88.0.1:8080/3.17/main' \
/etc/apk/repositories
apk update
apk add reposerve
reposerve --help
Posting packages
You can test the upload functionality by posting a package (aaudit-0.7.2-r2.apk
) we will
preliminarily download from the official alpine repository. Exit the container and execute directly
from the host :
# get a random official package
version=3.17
repo=main
arch=x86_64
file=aaudit-0.7.2-r3.apk
curl -O https://dl-cdn.alpinelinux.org/alpine/v"$version"/"$repo"/"$arch"/"$file"
# publish the package to our repo using the same parameters
curl \
-F version="$version" \
-F repo="$repo" \
-F arch="$arch" \
-F file=@"$file" \
http://localhost:8080/upload
If you go back to http://localhost:8080 you should see the new package available, and you can check that the package has been signed and the index updated.
Of course, you should deploy the apk
server behind HTTPS and use JWT authorization in production. See
reposerve
documentation.
Conclusion
This really was not that hard. We leveraged alpine build tools to correctly package an executable we
can now use on totally different contexts. I hope you are convinced that this approach is far
better than stacking build instructions in a Dockerfile
.
What’s next
Being able to streamline the creation of packages and images for production needs obviously more work. You must for instance :
- Correctly protect and deploy the
apk
keys (I use Vault), - deploy and secure a package repository with HTTPS, and use JWT to secure your catalog,
- and set up your CI/CD pipelines.
You can find in another blog post how I overcame these challenges: A better way to build containers images
Éric BURGHARD
Related posts
Managing roles for PostgreSQL with Vault on Kubernetes
Vault has a database secret engine with a PostgreSQL driver that helps to create short-lived roles with random passwords for your database applications, but putting everything in production is not as simple as it seems.
Building an alpine golden image
How to build an alpine image to base all your containers on.
Intalling Kubernetes with cri-o inside flatcar Container Linux
How to run containers without dockershim / containerd by installing cri-o with crun under flatcar Container Linux.