Whether you are into software development, DevOps or test engineering, if you have some work experience with Docker, you are most likely already familiar with Alpine Linux.
Alpine has gained great popularity in recent years and nowadays is probably the most favored Linux for Dockers. Originally designed for routers, it is a secure, fast, feather-light Linux: a basic Alpine base image takes as little as 5 MB, orders of magnitude less than other popular Linux distros (Ubuntu is 188 MB, for comparison). That fact makes it an ideal choice as the base system of Docker images, where small size is desirable, and specifically for OpenJDK Docker images that otherwise take up several hundred MBs.
I’m a big Linux fan, and enjoy experimenting with new environments, so I was very excited when I was tasked with porting the OverOps agent to native Alpine Linux! But before I could get started with any actual porting work, I had to set myself a proper Alpine development environment.
In this post, I’ll cover my experience in setting up an Alpine Linux workstation for C++ and Java development, with some hopefully useful Alpine know-hows, tips and resources.
First, let’s take a glimpse of the main features of Alpine Linux.
The project homepage summarizes it nicely: “Alpine Linux is an independent, non-commercial, general purpose Linux distribution designed for power users who appreciate security, simplicity and resource efficiency.”
The most notable security feature is a hardened, specially patched Linux kernel.
All Alpine executables are built as PIE – Position Independent Executables, which do not depend on absolute memory addresses for their correct operation, much like shared libraries. Such executables are subject to Address Space Layout Randomization (ASLR), a technique in which the kernel dynamically shifts programs between random memory locations, which is useful for preventing certain kinds of attacks.
This feature is implemented in all Alpine binaries, and allows stack overflowed programs to exit gracefully instead of crashing horribly. It is typically implemented by building with a special C/C++ compiler option (-fstack-protector-fstrong), that adds stack canaries to the program code. These are compiler generated variables that mark the end of the allocated stack frame in each function, and when overridden, indicate that the stack has overflowed and trouble is ahead, much like “canaries in a coal mine”.
Though not a feature per se, Alpine’s minimalistic nature also makes it safer and less prone to attacks, since it carries little to no excess baggage and therefore the possible attack surface is much smaller, especially compared to other Linuxes. Unfortunately, as with any other system, it is not 100% bulletproof, as uncovered by this recently found and fixed exploit.
So, Alpine is as secure as it can be, but its small footprint is what really sets it apart from the other Linuxes. It is built around two components that make it especially compact.
Busybox is an all-in-one, multi-purpose binary. Named by its authors “The Swiss Army Knife of Embedded Linux”, it provides the core functionalities for dozens of standard programs, such as awk, cp, grep, gzip, sh and top. All programs are symlinked to /bin/busybox, which identifies the program to run according the name it was executed with. It offers high compatibility to the GNU counterparts, typically with a reduced feature set, but worry not – you can usually get the full GNU functionalities by installing the package of the same name, such as grep or tar, or bundle packages like coreutils.
Busybox is significant to Alpine’s small size – using a single static binary reduces the overhead of multiple executables and allows the resulting binary to be effectively size optimized. On Alpine 3.8, it is squeezed into just 780 KB of goodness.
The musl libc library is a compact and compelling alternative to GNU’s libc library, glibc, which is the defacto libc implementation used in most Linux distributions. Compared to glibc, musl is tiny in size: it weighs only 572 KB on Alpine 3.8, compared to glibc’s 3 MB on Ubuntu 16.04, for example (3 MB is the combined size of libc.so and libm.so; in Alpine libm.so is symlinked to musl.so). But, there’s much more to musl than its small size: it offers stricter POSIX conformance, improved safety and competitive performance. This comparison chart from ETA labs, authors of musl, gives a very detailed picture of the differences between the libc implementations and highlights musl’s strengths.
Sadly, there’s a catch: having musl as the default libc implementation introduces a major compatibility issue with other Linuxes that are based on glibc. Almost any Linux program is dynamically linked with libc, and while glibc Linux binaries will link against libc.so, Alpine binaries will link against musl.so. Consequently, Linux executables that were built on glibc distros such as Ubuntu and RedHat, will not be able to run on Alpine, at least not out of the box.
A neat and simple package manager called apk. If you’re already familiar with the apt package manager, there’s no getting used to apk: the basic commands are add, del, search and update, and you’re good to go. If you’ve written Alpine Dockerfiles, you should already know apk pretty well.
The default shell in Alpine is the busybox provided ash. As with anything in Alpine, it is also quite minimalistic, and lacks some shell convenience features, such as auto completion. If you’re into bash, like myself, simply install the bash apk package, and use it as your default shell.
So, after some sniffing around, it was time to get started with setting up an Alpine work environment. But what environment should it be?
As I saw it, there were two options: one, using an Alpine Docker for my development work, and two, bringing up a proper Alpine workstation on my laptop. This was my first real encounter with Docker, so I wasn’t sure whether I should deep dive in that direction, since I wanted to get up and running quickly.
Also, at that point, I thought that Docker is essentially a disposable environment that may not be ideal for day to day development (only later I realized that Docker could come handy for that purpose, too). The Alpine workstation option seemed much more compelling, but having no experience with Alpine either, I wasn’t sure how comfortable it would be for development; with the Docker option, at least I still had my Ubuntu desktop to work with.
Finally, I chose option two, and rushed into installing Alpine on my already dual-booted Ubuntu-Windows Dell XPS 15.
Happy and jolly, I headed over to the Alpine downloads page. It offers the latest Alpine version (3.8.1, as of writing these lines), available in 8 different ISO flavours. It turns out, Alpine runs on pretty much anything, from servers, dockers and VMs, to embedded ARM devices and Raspberry Pi. Out of the x86_64 images, I contemplated between “Standard” and “Extended”. Since I was aiming for a full fledged desktop environment, I figured Extended would make a better starting point (which proved correct later on) and hit the download button. Alpine ISO – check.
Now comes the real fun part – install time! The first Google hit for “install alpine” is the installation page at the Alpine Linux Wiki. The Wiki is an incredible source of information, with just about everything there is to know about Alpine. Really awesome job by the Alpine team.
As described in the installation page, Alpine could be installed in three different modes:
Clearly, option 3 was the most suitable one for me. Therefore, I proceeded with the below steps on Ubuntu for installing Alpine:
That was about it. Just a few hours’ work, and I could now log in into my brand new Alpine Linux installation, triple booted with Ubuntu and Windows… Sweet!
The GRUB part was a bit tricky, though. After installing Alpine and updating GRUB, a new menu entry appeared: “unknown Linux distribution (on /dev/nvme0n1p9)”. Optimistically, I clicked it, but instead of the expected “Welcome to Alpine” prompt, after a few seconds of booting I got a kernel panic… Yikes!
It took some experimenting to figure out what was wrong: I did not take care of loading the appropriate kernel modules for my filesystem, ext4 specifically, and update-grub2 didn’t take care of that either. So, I used the Ubuntu GRUB entry as a starting point, and dropped the following menu entry in a new file under /etc/grub.d:
Now that I was able to successfully boot into Alpine, it was time for some basic setup steps.
First and foremost, I needed to get a working internet connection.
My laptop doesn’t have a wired network socket, and my USB RJ45 adapter was broken, so my only option was Wi-Fi. This Alpine Wiki page, “Connecting to a Wireless Access Point”, describes the Wi-Fi setup procedure very clearly, so I was very quickly able to connect to my work Wi-Fi network (specifically, I followed the “Manual Configuration” steps).
For getting started with Wi-Fi on Alpine, there are two essential packages that need to be installed: wireless-tools and wpa_supplicant. It turns out, my bet on the Extended ISO paid off: it includes these packages in its offline apk database, in contrast to the standard ISO. This saved me the trouble of using a colleague’s computer for downloading these packages from the Alpine packages catalogue (which is, BTW, pretty awesome!), copying them to a USB stick, mounting it to Alpine and installing the .apk files manually.
Now that I had network connectivity, I was able to update the apk repository indices and install some convenience packages, such as bash, coreutils and nano.
Feels like home already!
At this point, I had a decent working Alpine command line, so I was good for prepping the build environment.
Similar to Ubuntu’s build-essential, the build-base meta-package is a good starting point for installing the most common build tools and utilities, including g++, make and binutils. For building our C++ stack, I needed several other packages, including cmake and linux-headers.
Some C++ projects are only compatible with certain compiler versions, and specifically, Alpine 3.8 ships with gcc 6.4.0. The official Alpine repositories do not contain previous major gcc versions, as far as I could find, so if you’re needing a different gcc version on Alpine, chances are you’ll find it (or a compatible version) at the musl-cross-make project page: https://github.com/just-containers/musl-cross-make/releases. I’ve personally tested gcc 4.9.4, and it worked perfectly. Great job by these guys, which could be a real lifesaver for some.
Next, installing Java. The official Alpine JDK implementation is OpenJDK, brought to us by the OpenJDK IcedTea project. OpenJDK versions 7 and 8 are available, and could be downloaded from the Alpine apk repositories. Unfortunately, there are currently no GA builds for Java 9, 10 and 11 (see project Portola homepage and this github discussion for current status). For now, I was satisfied with the openjdk8 package.
Finally, expecting times of fierce debugging, I went ahead and installed gdb and strace, as well as musl and OpenJDK debug symbols, available in the musl-dbg and openjdk8-dbg packages respectively. Bring it on, Alpine!
Tip: for bleeding edge and latest packages, you may want to add the edge repository to your /etc/apk/repositories file, as described in the package management page. For example, lldb is currently only available in edge. However, edge packages are experimental, so use them with care.
I must admit, I first imagined Alpine as an exotic command line environment and didn’t see myself even using the mouse, so I was happy to learn Alpine is a capable desktop environment, too!
Alpine has a variety of desktops available, including GNOME, MATE and Xfce. I wasn’t yet familiar with Xfce, so I decided to give it a try. As always, the Alpine Wiki is a great place to start. The “Xfce setup” page details all the steps needed for installing and starting Xfce. Most importantly, the packages to install are xfce4 – the desktop itself, and alpine-desktop. The latter is a very comprehensive meta-package that delivers the default Alpine desktop experience, including the Firefox web browser, AbiWord editor, Audacious sound player, and many others.
Tip: Don’t forget to install a font package as well, such as font-noto. Otherwise, you’ll end up with weird blank squares for text, and some GUI apps may crash or misbehave. This took me a decent amount of time to figure out!
The final piece missing in my Alpine puzzle was finding proper IDEs for both C/C++ and Java development. Here, the musl-glibc compatibility issue becomes really painful. For example, Eclipse CDT builds compatible with musl are not to be found, so without the glibc compatibility layer, it’s not possible to run Eclipse on Alpine. I’m comfortable with the command line, but I really wanted a proper IDE at hand, especially for debugging.
Luckily, we have the JetBrains IDE family. IntelliJ, CLion and other IDEs from JetBrains all run on the JVM, and do not depend directly on the underlying libc implementation. Since the OpenJDK JVM is fully compatible with musl, any standard Java app should run fine out of the box, and so do IntelliJ and CLion. I’m a big IntelliJ fan, so I was thrilled to see that it’s working just as well on Alpine. What a relief!
It’s important to note that while IntelliJ is freely available to everyone in its community edition, CLion is only free for a 30-day evaluation period (though, students can apply for a free academic license). For now, this was enough.
That was it – I was set up with my Alpine desktop environment!
Alpine has become one of the most important Linux distros around, thanks to its widespread use in containers. While being very slim and minimalistic at its core, it could be easily stacked up with most programs and functionalities we’re accustomed to from other popular Linux distros. With some effort, an Alpine installation could be turned into a proper desktop environment that has nothing to be ashamed of.
However, there’s a learning curve to Alpine, so it’s surely not for everyone. Since it has a relatively small user base, there’s less compatible software available, and you may find that the package of need is not yet supported on musl Alpine. Alpine comes with excellent documentation, but on the other hand, there are much less troubleshooting resources and discussions (think StackOverflow), so you may end up having to diagnose and solve problems on your own.
All in all, Alpine has many great things going for it, and I currently take much pleasure in working on it. Hope you will, too!
Fast forward a few weeks, as I got more familiar with Docker, I became acquainted with data volumes, which are useful for sharing folders between the host and the docker container. I also learned that when properly set up, Docker will happily run GUI apps, too.
Therefore, essentially, the entire Alpine build and desktop environment could be encapsulated in a reusable Docker image, and our projects source could be shared to the Docker and used for development on the Alpine container.
So, without further ado, here’s an Alpine 3.8 Dockerfile with IntelliJ Community edition + the tools and utilities mentioned earlier. While not perfect or complete, this image may be a good starting point for your very own Alpine dockered desktop. For running the image and starting IntelliJ, follow the instructions at the top of the Dockerfile. GUI is enabled by granting Docker access to the host X11 server.
Link to Dockerfile on GitHub: https://github.com/shaharv/docker/blob/master/alpine/dev/Dockerfile
Enjoyed reading this blog post or have questions or feedback?
Share your thoughts by creating a new topic in the Harness community forum.