As an avid container user and occasional distro hopper Distrobox has my interest peaked.

Containers, containers and more containers

My main development setup is based on a read-only openSUSE MicroOS installation, meaning my host system has few if any tools or non-essential software installed. I can, and sometimes do, re-use my home as well which becomes a lot easier when it also has my tools and apps inside it. Plus the fact that updates to apps or development environments don’t really disturb anything else.

For this reason I’m also somewhat fond of the fact that distrobox is very distro-agnostic and officially supports being installed to your user’s home:

curl -s https://raw.githubusercontent.com/89luca89/distrobox/main/install | sh -s -- --prefix ~/.local

Note: Be sure that your prefix of choice is in your PATH!

A couple of things to consider

Since I like things confined to userspace my primary use case is also running everything as a regular user. Naturally that means your system has to be setup for it. It’s easy but also potentially hella confusing when you haven’t done that, so let’s make sure we have usable ID mappings before getting further into it:

sudo usermod --add-subuids 524288-1878523903 --add-subgids 524288-1878523903 $USER
grep $USER /etc/sub{u,g}id
podman system migrate

The range we are choosing here is reserved for containers by systemd, which is relevant when dynamic users such as managed by systemd-homed come into play. If you’re interested in the details podman docs is where it’s at.

Although you can use other container managers I’m going to assume that you use podman or that you can swap out the executable in relevant commands going forward.

In the name of convenience it may be a good idea to also define your default image so that you don’t need to specify it unless you specifically want something else:

export DBX_CONTAINER_IMAGE=registry.opensuse.org/opensuse/tumbleweed:latest

Any time a new container is created distrobox will use this image.

How does this work then?

The first time you use a container it needs to be created. Although distrobox will also prompt you to do this when you’re trying to enter a non-existent container this is handy to use in e.g. setup scripts you might have to prepare your environment on a new machine or for a new user:

distrobox create

This will pull an image if needed and populate it with some necessary tooling. Next, as will the instructions also tell you, enter:

distrobox enter

And that’s it. If all you needed is a container that mirrors your user setup with generous access to files and host access for networking, devices or your graphical session you’re good to go.

sudo make me a container

Another reason you may need to create a container explicitly is… superuser powers. It’s not the primary use case but now and then real root is useful. If like me you’re into graphical prompts via polkit you can trivially enable them by overriding the sudo helper:

DBX_SUDO_PROGRAM="pkexec" distrobox create --root

I like my services managed by systemd

What’s I think a unique feature at this point is that you can get support for systemd within containers out of the box. By default containers are isolated as you would expect. A command-line switch enables them:

distrobox create --init
systemctl list-units

Now you can not only develop your services inside a container but allow it to communicate with the init system running on the host system. This can otherwise be rather tedious to sort out.

Note: Check that your preferred image supports it, though, some may not have systemd installed. The official Tumbleweed and Leap images do.

Save me the hassle, please

One thing you actually get from the container runtime that’s nevertheless quite handy is saving and restoring of images. Besides always having the option to recreate containers from scratch you can in fact back them up:

# Backup a snapshot of your container
podman container commit -p mybox myimage
podman save myimage:latest | gzip > myimage.tar.gz
# Restore your existing snapshot and make a container from it
podman load < myimage.tar.gz
distrobox create --image myimage:latest

It does help, though, to remember to do this before breaking an image. If you forget to do so you’re back to distrobox create. 😉️

Architectures are not that big of a deal

If that wasn’t impressive enough yet, how about running images on different architectures? Again this is strictly speaking relying on your container runtime and Linux kernel magic. Assuming you have a suitable qemu installed (i.e. qemu-uefi-aarch64 on openSUSE) you can actually do this on an x86 machine:

distrobox ephemeral -a "--arch=arm64"

The -a actually passes container manager-specific arguments like in this case the architecture. Assuming the image provides it, it will just do what you would expect.

Oh, and in case you’re wondering… I was creating an ephemeral container there. This is handy for when you just need another clean container that you don’t intend on keeping around. Like its persistent cousin it honors DBX_CONTAINER_IMAGE so it’ll automatically use your default image unless you specify --image. In the above case it will get the arm64 version of Tumbleweed since that’s what we specified earlier.

Convenient integration into my workflow

If you use a multiplexer like zellij adding distrobox to your default.kdl can make life even easier:

tab name="distrobox" {
    pane {
        command "distrobox"
        args "enter"
        close_on_exit true
    }
}

In order to simplify development workflows where some of your tools live on the host system, or perhaps other containers, I also use a command not found handler that’s aware of the hybrid nature of the system:

command_not_found_handle() {
  # do not run in a pipe
  [ ! -t 1 ] && return 127
  
  # use command-not-found if installed
  command -v cnf >/dev/null 2>&1 || cnf() { echo $1: command not found; }
  # elevate sudo priviledges if needed
  if [ -x "/usr/sbin/$1" ]; then
    sudo /usr/sbin/$*
  elif [ -e /run/.containerenv ]; then
    # try to execute the command on the host
    distrobox-host-exec "${@}" || (cnf $1; return 127)
  else
    cnf $1; return 127
  fi
}
[ -n "${ZSH_VERSION-}" ] && command_not_found_handler() { command_not_found_handle; }

On openSUSE there’s the scout-command-not-found package to provide cnf which looks up the package containing the command you attempted to run when it wasn’t actually installed. This mechanism is leveraged here to do two more things. First priviledged commands are looked up so you don’t need to type sudo unnecessarily when there’s no other choice. Second distrobox-host-exec is used to call into the host system. This makes things like xdg-open, podman and flatpak work from within the container.

Incidentally this also works with non-distrobox containers that have access to the distrobox tooling and it’s more generic than flatpak-spawn --host.

My appetite is wet, what’s next?

You can find many more details on the neat tricks we discussed on the distrobox project page.

I also wrote about toolbox before which comes bundled with MicroOS and might be worth a look as well.

Now go enjoy your containers!