I’ve recently become interested in systemd-nspawn and have even managed to run graphical applications in it! Read on to learn about my (mis-?)adventure into this process.

What is systemd-nspawn?

Lennart Poettering once referred to it as “chroot on steroids” and that is a fantastic tagline in my opinion. If you want to learn more, please go consult that video and other resources. I am not qualified enough to tell you intimate details.

For the purposes of this post, I am going to treat it as a lightweight virtual machine. Is this the best way? Probably not, but it is something we can do with the technology and I wanted to show that off.

For clarity, I am going to focus on the following:
NO X11
NO ALSA
NO PulseAudio
NO USB Redirection
YES Wayland
YES XWayland
YES PipeWire
YES GPU Acceleration

Getting Started

For this demonstration, I am going to spin up Firefox inside a nspawn container.

  1. Create Container Directory
# Create container in system location
sudo mkdir --parents /var/lib/machines/arch-gui-cnt
  1. Install Base System
# Install minimal Arch system
# the -c is great here because it uses the host pacman cache to skip redownloading packages
sudo pacstrap -K -c /var/lib/machines/arch-gui-cnt \
    base \
    firefox \
    pipewire-jack \
    ttf-ibm-plex \
    vulkan-radeon
  1. Initial Container Setup
# Enter container for initial setup
sudo systemd-nspawn \
    --directory=/var/lib/machines/arch-gui-cnt

Your terminal should resemble this now:

░ Spawning container arch-gui-cnt on /var/lib/machines/arch-gui-cnt.
░ Press Ctrl-] three times within 1s to kill container; two times followed by r
░ to reboot container; two times followed by p to poweroff container.
[root@arch-gui-cnt ~]#

We want to do this for an interactive experience.

Configuring the Container

  1. Locale Setup
# setup locales
sed --in-place --expression '/^#en_US.UTF.*/ s/^#//' /etc/locale.gen &&\
locale-gen

Note: This is run inside the container.

  1. User Setup I am going to use the tcloud user here.
# setup user
groupadd --gid 1000 tcloud &&\
useradd --create-home --uid 1000 --gid 1000 --groups wheel --shell /usr/bin/bash tcloud &&\
echo "hunter12" | passwd --stdin tcloud

Note: This is run inside the container.

  1. Configure Some Static Variables
echo 'export XDG_RUNTIME_DIR=/run/user/1000' >> /home/tcloud/.bash_profile
echo 'export DISPLAY=:0' >> /home/tcloud/.bash_profile
echo 'export PIPEWIRE_RUNTIME_DIR=/run/user/1000' >> /home/tcloud/.bash_profile
# suppress accessibility bus warnings
echo 'export NO_AT_BRIDGE=1' >> /home/tcloud/.bash_profile

Note: This is run inside the container.

  1. Exit Container
exit

Note: This is run inside the container.

Running the Application

Back on the host, launch Firefox inside the container.

sudo systemd-nspawn \
    --machine=arch-gui-cnt \
    --directory=/var/lib/machines/arch-gui-cnt \
    --private-users=pick \
    --private-users-ownership=auto \
    --user=tcloud \
    \
    --bind=/dev/dri \
    --bind=/dev/snd \
    --bind=/dev/kfd \
    --property=DeviceAllow='char-drm rwm' \
    \
    --tmpfs=/run/user/1000:mode=1777 \
    --bind-ro=$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY:/run/user/1000/$WAYLAND_DISPLAY:idmap \
    --bind-ro=$XDG_RUNTIME_DIR/pipewire-0:/run/user/1000/pipewire-0:idmap \
    --bind-ro=$XDG_RUNTIME_DIR/pulse:/run/user/1000/pulse:idmap \
    \
    --setenv=WAYLAND_DISPLAY=$WAYLAND_DISPLAY \
    --setenv=XDG_RUNTIME_DIR=/run/user/1000 \
    --setenv=PULSE_SERVER=/run/user/1000/pulse/native \
    \
    --as-pid2 \
    firefox