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 to start 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, otherwise tzdata 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 with sed.
  • 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:

docker run -it --name xpra-1 -p:8080:8080 xpraa
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.

Xpra empty desktop running in a container

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.

Xpra running many terminals from different containers

To explain the Dockerfile:

  • set the X11 DISPLAY environment variable - if you have different Xpra servers, then don’t specify here, but use docker run -e DISPLAY=:80 ... instead,
  • then install what you want, here I install a few applications:
    • Firefox
    • Kate, a simple text editor which uses Qt
    • LxTerminal terminal emulator
  • create a user usr, assigning to the sudo 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).

Xpra with Firefox and Kate

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.
  • 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 with docker stop xpra-apps-1
    • is not deleted, and can be re-started with the file system intact, docker start xpra-apps-1.

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!