Beni's website

← Back to blog posts

Why?

I’ve been playing around with Alpine Linux lately, and I’ve found myself missing a couple of nice-to-have packages, and I’ve also been wanting to package my own software.

I usually run Alpine on low-resource systems such as old laptops, or something like a raspberry pi, so having to compile a whole bunch of software from source individually, or having to mess around with just downloading prebuilt binaries (if they exist at all) from Github releases and manually “installing” them became a real hassle.

I’ve also considered contributing some of these packages to the official repos, maybe I will in the future. For now, however, this was a quick way to get access to these packages, and it also allows me to add my own programs, and things that wouldn’t make sense to be in the official repos.

What?

Alpine uses it’s own build system, with APKBUILD files to describe how to build a package, and the abuild program to build them.

Alpine has a couple of official repositories: main, community, and testing. What I want to do, is make my own, where I can upload my own packages that are independent of the official ones, but can also depend on them.

The Alpine Wiki has a great tutorial on how to write the files, and how to build them locally. However, it’s all from the context of contributing to their offcial repos, and it has practically no documentation on how to go about a custom package repo.

How?

The entirety of the official repos can be found in a git repo called aports. It’s important to note, that this is where all the APKBUILD files are, but this isn’t what a user of the distro interfaces with. Rather, the Alpine team builds all of these packages for a variety of CPU architectures, and the binaries are available on a worldwide array of mirrors.

Development

Similar to aports, I made a git repo with all the build scripts. You need a root directory that stores the build scripts, and the name of this directory will specify the name of the package repo.

When I’m writing the build scripts, I want to be able to test them locally to make sure it works before publishing it. I don’t run Alpine on any of my main computers, so I used this script to get myself an Alpine environment without having to fiddle with containers or even VMs.

Building

I decided to use Github Actions for building the binaries, mainly because it’s free. Currently, it spins up one VM per CPU architecture (64-bit x86 and arm). That’s fine for now, but it probably won’t scale when I add more packages, since that means all the packages themselves are built sequentially, rather than in parallel. The fact that abuild doesn’t seem to support caching or incremental compilation doesn’t help either.

I’m using this action to be able to run an Alpine environment on Github Actions, which uses Ubuntu by default. Thanks to this action, setting up the packaging environment was relatively simple. I put my abuild config file in the repo, along with the public key, and I put the private key in Github secrets, and I manually place that on the runner, as part of the setup. (more on keys later) Abuild doesn’t like to be run as root, thankfully this action automatically creates a normal user for me to use, I just have to give it some permissions to be able to build packages.

The actual build process is just a shell script, that iterates every package definition, and runs abuild on it. This will automatically create the “custom repository” on the system, and add the built archive file to it, as well as update the repo index file with every newly built package.

Right now, the repo will be rebuilt everytime I change a build script, and also once a week, to keep up with the official repos. This is quite expensive computationally, but as long as Github doesn’t charge me for that, the only downside is the speed of the builds, especially because the arm build has to be virtualized, it’s over 5x slower than the x86 one, and the published files will only be replaced once the whole build finishes for the sake of consistency.

Please note, rebuilding the whole repo everytime is not only wasteful, but also incorrect, since I don’t advance the build numbers each time. I want to figure out a solution for this, but for now this setup works, and making it correct might just be too much effort, but maybe I’ll figure it out, and I’ll maybe even update this post with it.

See the Github Actions build script here.

After all the packages are built, this resulting directory is ready to be published.

Publishing

After considering a few options, I’ve decided to host the binaries in a cloud storage bucket. I’m using Cloudflare R2, because it has a very generous free tier, and because it can do some cross-region replication magic automatically, removing the need for regional mirrors. I set it up to allow public access with a custom domain, so that all the files are easily available over HTTPS.

I’ve also considered just using another git repo, and straight up using Github for the hosting, but that didn’t seem like the right tool for the job. Also, I believe if the usage became too much, Github could decide to pull the plug on the project. It’s utterly improbable for my silly little repo to generate that much usage, especially because it’s mostly only useful for me, and I wouldn’t advise anyone else to use it, because I’m not making any commitments that it won’t disappear next week.

Either way, I decided on the cloud bucket, because that’s also free and it seemed a lot less “hacky”.

After the builds, this action uploads the files, and with that, the packages are available to the world.

Using

To use the repo, grab an install of Alpine Linux, and edit the /etc/apk/repositories file. On a new line, add the url of the cloud bucket, and now apk (the Alpine package manager) will fetch it everytime you run an update.

As for security, Alpine uses signing keys, to sign the released binaries. This is used to verify that the packages came from the actual authors (usually the Alpine team). During the setup of the packaging environment, I needed to create my own key. This key is then used by the builder system, to sign all my packages. To make the target system trust this key, I added the public key to the /etc/apk/keys directory, and apk will happily accept my custom packages.

Conslusion

Creating a custom package repo is probably a useless undertaking for most, but I found it to be interesting, and maybe even a little bit useful.

Now, when I install Alpine on an old computer, I don’t need to compile a bunch of programs, I just run a little script, that installs my custom repo, and I can freely install my own custom pieces of software in a matter of seconds.

The code for all this is on my Github. I made it public as an example of how to make a custom package repo, but the actual repo isn’t meant to be used by other people, so plese don’t. One day I might just change the signing key, or take it offline entirely, breaking any install that relies on it, and when (not if) that happens, I’m not going to tell you in advance.