There are multiple ways to run Linux GUI applications in a Docker container, but today I’ll highlight a method that I found interesting: using Xpra to forward X11 screens from containers to a web browser. This post is based on Eugene Yaremenko’s docker-x11-bridge, which I simplified.
Introduction
The main program needed is Xpra, “an open-source multi-platform persistent remote display server.” Xpra can forward an X11 windows to a HTML5 web browser (tested with Firefox).
It’s possible to just install this in a container and install any applications you use there, but Eugene’s method uses multiple containers:
- an Xpra container,
- one (or more) “application” container(s) to run Linux GUI applications.
An application container would mount a shared X11 folder to pass display instructions to a virtual display, and xpra forwards that to a web-based client. Kinda? :P
This way, we have have multiple containers running different applications. This method is akin to my Uni’s SunOS setup - a single multi-user UNIX server, with users on X11 clients.
Pros:
- [+] Works with modern browsers on most platforms, even on iPad!
- [+] No need for remote desktop through RDP or VNC protocols, or an X11 client like XQuartz.
- [+] Multiple application containers for different purposes, which may be useful for development. In fact, it’s also possible to use different Linux distributions, and below I use both Ubuntu and Alpine.
Cons:
- [-] Slower than RDP and native X11 clients, and a bit less stable... not everything works!
- [-] I don’t know how to “redirect” sound from the application container (via SSH) to Xpra.
- [-] No application menu in Xpra, since the applications run in a different container.
Alternatives
On a Linux desktop, you can use the native X11 server and the window manager. Use x11docker to interact with applications in containers. On macOS, you’ll need to install XQuartz.
Also, if all you want is remote access to a full featured Linux desktop via RDP or VNC, try projects like Tiny Remote Desktop by SoffChen, which also has a built-in HTML5 VNC client as an alternative.
Xpra Container
I tried both Ubuntu and Alpine base.
The Ubuntu image:
- runs the latest Xpra v4.0.6-r28351(11 Jan 2021) from the Xpra repo.
- supports OpenGL and can support sound through PulseAudio - though playback is extremely delayed to the point of being unusable.
- this version has a bug - minimizing any window caused a segment fault immediately.
- shuts down cleanly, stopping all child processes properly.
- is huge, at 1.11GB.
The Alpine image:
- runs the older Xpra v3.0.9-r1(14 Apr 2020)
- can’t run OpenGL and PulseAudio (some dependency must be missing).
- in this version, minimize works, but there is a warning about an issue setting DPI: “DPI set to y x z (wanted 96 x 96) you may experience scaling problems, such as huge or small fonts, etc to fix this issue, try the dpi switch, or use a patched Xorg dummy driver”.
- stopping Xpra via Ctrl-Ccauses a segment fault.
- is tiny, at 252MB since there is less included e.g. no sound.
Xpra is pretty much self-contained as long as required dependencies are installed. To explain the Dockerfile
:
VOLUME
creates a mount point that can be externally mounted.- the
RUN
section installs whatever is needed. - and finally,
ENTRYPOINT
is the command tostart
Xpra serving DISPLAY:80
, and with these parameters:--bind
the HTML5 interface on port 8080.- run Xpra in the foreground
--no-daemon
so that it exits with Ctrl+C (SIGINT) - after Xpra is up, to run
xhost +
to disable access control, so that clients can connect from any host (this is critical). - the remainder
...=no
parameters just suppress warnings by explicitly disabling WebCam and sessions announcement via mDNS.
Ubuntu Xpra Image
For Ubuntu:
apt update
needs to be run non-interactively, otherwisetzdata
will prompt for timezone input.- the Xpra setup in the Ubuntu repo does not work, so I use the Xpra repo and the latest Xpra version. This requires additional steps to add the repo and
wget
the PGP signature. - Since Xpra runs as root, I manually create the folder Xpra uses.
- PulseAudio does work and can be added in if required.
- And it is possible to launch a terminal emulator (or any other application) when a client connects - this provides access to the Xpra container itself.
Build this Dockerfile
with docker build -t xprau .
:
FROM ubuntu
VOLUME /tmp/.X11-unix
RUN apt update \
&& DEBIAN_FRONTEND=noninteractive apt install -y wget gnupg xvfb x11-xserver-utils python3-pip \
# pulseaudio lxterminal \
&& pip3 install pyinotify \
&& echo "deb [arch=amd64] https://xpra.org/ focal main" > /etc/apt/sources.list.d/xpra.list \
&& wget -q https://xpra.org/gpg.asc -O- | apt-key add - \
&& apt update \
&& DEBIAN_FRONTEND=noninteractive apt install -y xpra \
&& mkdir -p /run/user/0/xpra
ENTRYPOINT ["xpra", "start", ":80", "--bind-tcp=0.0.0.0:8080", \
"--mdns=no", "--webcam=no", "--no-daemon",
# "--start-on-connect=lxterminal", \
"--start=xhost +"]
Alpine Xpra Image
For Alpine:
- the
apk add
is pretty straightforward, and simpler, but an old version... this old version is what I use. - I also need to fix a folder error in this version by replacing it using
sed
.
Build this Dockerfile
with docker build -t xpraa .
:
FROM alpine
VOLUME /tmp/.X11-unix
RUN apk add --no-cache py-cairo py-pip py-netifaces xhost dbus-x11 xpra \
# lxterminal ttf-dejavu font-noto \
&& sed -i "s+-config /home/buildozer/aports/community/xpra/pkg/xpra/etc/xpra/xorg.conf+-config /etc/xpra/xorg.conf+" /etc/xpra/conf.d/55_server_x11.conf \
&& pip install pyinotify \
&& mkdir -p /run/user/0/xpra
ENTRYPOINT ["xpra", "start", ":80", "--bind-tcp=0.0.0.0:8080", \
"--mdns=no", "--webcam=no", "--no-daemon", "--pulseaudio=no", \
# "--start-on-connect=lxterminal", \
"--start=xhost +"]
To upgrade Xpra in Alpine:
- get into the container via shell,
docker run -it --name xpra-1 --entrypoint sh -p:8080:8080 xpraa
to override the entry point, - check what Xpra version you want to use at https://xpra.org/src/
- download and re-compile the
APKBUILD
file using the commands below (good luck), replacing the version withsed
. - finally, manually start Xpra
xpra start :80 --bind-tcp=0.0.0.0:8080 --no-daemon --mdns=no --webcam=no --pulseaudio=no --start="xhost +"
. - of course all this could be added to the
Dockerfile
... but then the container will get larger!
wget -O- https://git.alpinelinux.org/aports/plain/community/xpra/APKBUILD > APKBUILD
sed -i 's/pkgver=3.0.9/pkgver=4.0.6/' APKBUILD
apk get abuild
abuild-keygen -aiy
abuild -F checksum
abuild -rF
apk upgrade -X /root/packages --allow-untrusted xpra
xpra --version
Run the Xpra Container
Finally, run the Xpra either container interactively -it
at this stage it is easier to see the output messages.
For Alpine:
docker run -it --name xpra-1 -p:8080:8080 xpraa
or for Ubuntu:
docker run -it --name xpra-1 -p:8080:8080 xprau
If all goes well, you will get something that ends like this with no major errors:
2021-01-13 23:57:51,988 xpra is ready.
2021-01-13 23:57:51,990 xpra GTK3 X11 version 3.0.9-r26127 64-bit
2021-01-13 23:57:52,054 uid=0 (root), gid=0 (root)
2021-01-13 23:57:52,062 running with pid 1 on Linux unknown unknown unknown
2021-01-13 23:57:52,085 connected to X11 display :80 with 24 bit colors
2021-01-13 23:57:52,271 1.9GB of system memory
Test a remote Xpra session
Now, in a web browser on your host, head over to http://localhost:8080/
. You should see an empty desktop, a bit like this. The menu at the top is is supposed to show the list of applications - but nothing is installed in this container. And FYI the second icon is the list of opened windows.
You can also connect to http://localhost:8080/connect.html
and configure Advanced Options including reversing the vertical scrolling direction.
Application container
Instead of adding applications to the Xpra image, we can use a different containers to run applications, and all can connect to the Xpra server simultaneously:
- A container using Ubuntu will be larger (890MB), but usually easier to use, easier to install - this is what I prefer.
- A container using Alpine will be smaller (516MB), but can be a bit fiddly.
Just be aware of what is running where. For example, you may have multiple terminals from different containers which is very confusing. Good idea to make sure prompts configured, e.g. root@host-xxxxxxx:/#
, usr@host-yyyyyyy:/#
, etc.
To explain the Dockerfile
:
- set the X11
DISPLAY
environment variable - if you have different Xpra servers, then don’t specify here, but usedocker run -e DISPLAY=:80 ...
instead, - then install what you want, here I install a few applications:
- create a user
usr
, assigning to thesudo
group, - no password to
sudo
, - switch to user,
- switch to work directory,
- start terminal.
Ubuntu Applications Image
Build this Dockerfile
with docker build -t xpra-appsu .
:
FROM ubuntu
ENV DISPLAY=":80"
RUN apt update \
&& DEBIAN_FRONTEND=noninteractive apt install -y wget gnupg sudo firefox kate lxterminal
RUN useradd -rm -u 1001 usr -G sudo -g root -s /bin/bash \
&& sed -i '/%sudo.*ALL/a %sudo ALL=(ALL) NOPASSWD: ALL' /etc/sudoers
USER usr
WORKDIR /home/usr
ENTRYPOINT ["lxterminal"]
Alpine Applications Image
Build this Dockerfile
with docker build -t xpra-appsa .
:
FROM alpine
ENV DISPLAY=":80"
RUN apk add --no-cache sudo firefox kate lxterminal ttf-dejavu font-noto bash
RUN adduser -S usr -G wheel -s /bin/bash \
&& sed -i '/# %wheel ALL.*/s/^# //' /etc/sudoers
USER usr
WORKDIR /home/usr
ENTRYPOINT ["lxterminal"]
Run the Applications Containers
The start up order is important, the Xpra container must be up first, and then only start the application container.
For Alpine:
docker run -it --name xpra-apps-1 --volumes-from xpra-1 --net xpra-net xpra-appsa
and/or for Ubuntu:
docker run -it --name xpra-apps-2 --volumes-from xpra-1 --net xpra-net xpra-appsu
If the container terminates with cannot open display: :80
then it’s because Xpra is not started, or needs to be re-started (because the build changed or for other reasons).
If all goes well, you you should see the terminal in your browser. Note that applications can’t appear in the Xpra application menu (the three-line hamburger) because there are no applications on the Xpra container. So, you have to run everything from the terminal manually:
- run the application either the foreground (
firefox
) or background (firefox &
), - closing or
exit
-ing the terminal will end the application container.
A note about Electron apps
With this setup, using Ubuntu, Electron App apps must have the --no-xshm
argument, so that the Chromium engine does not use shared memory - otherwise you will get a bank screen / empty window.
Install Visual Studio Code by downloading in Firefox as the user, and then:
sudo apt install -y ~/Downloads/code*.deb
code --no-xshm &
Install Signal:
sudo touch /etc/apt/sources.list.d/signal.list
echo 'deb [arch=amd64] https://updates.signal.org/desktop/apt xenial main' | sudo tee -a /etc/apt/sources.list.d/signal.list
wget -O- https://updates.signal.org/desktop/apt/keys.asc | sudo apt-key add -
sudo apt update
sudo apt install -y signal-desktop
signal-desktop --no-sandbox --no-xshm &
Conclusion
In my final setup:
- the Xpra container
- is run in the background with
docker run -d --name xpra-1 --net xpra-net -p:8080:8080 xpra
and - is stopped via the Xpra desktop Shutdown Server menu option under Server or with
docker stop xpra-1
- is not deleted, and can be re-started with the file system intact,
docker start xpra-1
.
- is run in the background with
- the application containers
- is first run in the background with
docker run -d --name xpra-apps-1 --volumes-from xpra-1 --net xpra-net xpra-apps
, - is stopped when the terminal is closed or
exit
-ed, or withdocker stop xpra-apps-1
- is not deleted, and can be re-started with the file system intact,
docker start xpra-apps-1
.
- is first run in the background with
If, however, the Xpra container is re-built, or it is deleted and run from scratch (i.e. docker rm xpra-1
followed by docker run ...
), then it it is not possible to start the stopped apps containers. The app containers also need to be deleted and re-run (i.e. docker rm xpra-apps-1
followed by docker run ..,.
).
That was a lot of work, but I do like this method for running GUI applications in containers. Hope you like it too!