Running GUI Applications in systemd-nspawn Containers
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.
- Create Container Directory
# Create container in system location
sudo mkdir --parents /var/lib/machines/arch-gui-cnt
- 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
- 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
- Locale Setup
# setup locales
sed --in-place --expression '/^#en_US.UTF.*/ s/^#//' /etc/locale.gen &&\
locale-gen
Note: This is run inside the container.
- User Setup
I am going to use the
tclouduser 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.
- 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.
- 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