Bonus: GUIs


This page is under construction.

Capabilities are less portable and not as "baked in" as the TUI support, but compose.mk can also be used to work with GUIs. Cross-platform GUIs and Docker is a complex topic though, and this page is still a work in progress.

This section has a growing collection of small demos for working with GUIs, sometimes combining multiple existing GUIs and even window managers into a single "application" in only a handful for lines.

See the next section for more detailed information about background and caveats. A quick overview though:

  • Jump to the Xephyr Demo if you're cool with linux only functionality, and looking for something with a native feel.
  • Jump to the Xpra Demos if you're looking for something cross-platform and web-based.
  • Jump to the hermetic GUI if you're interested in isolation and portability. It's technically a combination of text + web UIs for graphics, but it can run most GUIs, the graphics are surprisingly good, and the implementation is very clean.
  • For a native feel with MacOS, the XQuartz Demo is planned but incomplete. For now it's just tracking some resources that might be useful.

Background & Caveats


Demos here are so far mainly targeting Linux, and sometimes x11 and/or system packages might be required.

Despite amazing work with projects like x11docker1, Xephyr2, xquartz3, and xpra4 .. working with GUIs tends to be fragile, fiddly, and less portable than terminal apps. Any or all of those projects can in principle be combined with compose.mk, but the right approach to "batteries included" is much harder to land on.

All the projects mentioned are about solving the issue of getting a stand-alone XOrg server, but after you have one, you still need to decide how exactly to use it. Options here include stuff like forwarding graphics via a browser, VNC, ssh, sockets, etc. Searching for any combination of these tools + docker gives lots of results for this sort of thing, but much of the related work is old, archived, or unmaintained. Many tutorials along these lines will have lots of different files, and invite you to do lots of copy/paste. As usual with compose.mk we'll aim for something self-contained and try to minimize dependencies!

Just for fun, we'll also use CMK-lang for the examples here, which is a separate language that tends to clean up syntax, and is compiled to Makefile on demand.

Xephyr Demo


Caveats

This example only works on linux, requires x11 to be already started, and requires Xephyr on the host. (In debian-based distros, you can install it with something like apt-get install -y xserver-xephyr.)

The goal for this demo is a "composite" kind of application, similar to what many of the TUI demos are showing. We want to throw together "widgets" that might consist of smaller applications that are themselves dockerized, or are already available on the host. The feel should be "native", i.e. no port-forwarding, no browsers.

Similar to how we've already seen compose.mk leveraging tmux to get a geometry manager for console, we'll piggyback on i3 for a tiling window manager. As with tmux, we don't want to assume i3 is actually available on the host. Even if it is available on the host.. we want a sandboxed configuration for it that ships as part of the rest of the composite app we're building.

You can see the code for this below, it is complete and runs as-is.

Summary
#!/usr/bin/env -S ./compose.mk mk.interpret!
# Demonstrating compose.mk GUI capabilities with Xephyr and i3 window manager.
#
# USAGE: ./demos/cmk/xephyr.cmk gui

export APP_DIMS?=1024x500
export APP_DISPLAY?=:23

# Begin containerized version of i3 window manager and support files.
 gui.services
services:
  i3:
    image: compose.mk:i3
    hostname: i3
    build:
      context: .
      dockerfile_inline: |
        FROM ${IMG_UBUNTU_BASE:-ubuntu:22.04}
        ENV DEBIAN_FRONTEND=noninteractive
        RUN apt-get update -qq 
        RUN apt-get install -qq -y make procps
        RUN apt-get install -qq -y i3 i3status x11-apps xterm feh mesa-utils
    entrypoint: i3
    working_dir: /workspace
    volumes:
      - ${PWD}:/workspace
      - ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
      - /tmp/.X11-unix:/tmp/.X11-unix
    environment:
      DISPLAY: ${APP_DISPLAY:-23}
    configs:
      - source: i3conf
        target: /root/.config/i3/config
      - source: i3statconf
        target: /root/.config/i3status/config
configs:
  # https://i3wm.org/i3status/manpage.html
  # https://github.com/i3/i3status
  i3statconf:
    content: |
      general {
        output_format = "i3bar"
        colors = true
        interval = 5
      }
      order += "cpu_usage"
      order += "disk /"
      order += "tztime local"
      cpu_usage { format = "CPU: %usage" }
      disk "/" { format = "Disk: %free" }
      tztime local { format = "%Y-%m-%d %H:%M:%S" }
  # See https://i3wm.org/docs/userguide.html for a complete reference
  # Set mod key to Alt (Mod1)
  # Use Mouse+$mod to drag floating windows
  # Kill focused window
  # Change focus between windows
  # Split window
  # Toggle fullscreen
  # Layout toggle (stacked, tabbed, default)
  # Floating toggle
  i3conf:
    content: |
      set $$mod Mod1
      floating_modifier $$mod
      bindsym $$mod+Shift+q kill
      bindsym $$mod+h focus left
      bindsym $$mod+j focus down
      bindsym $$mod+k focus up
      bindsym $$mod+l focus right
      bindsym $$mod+v split v
      bindsym $$mod+b split h
      bindsym $$mod+f fullscreen toggle
      bindsym $$mod+s layout stacking
      bindsym $$mod+w layout tabbed
      bindsym $$mod+e layout toggle split
      bindsym $$mod+Shift+space floating toggle
      bar { status_command i3status }


# Top-level helpers, these require Xephyr 
# on the host and starts a stand-alone XOrg server.

xephyr.init: xephyr.require xephyr.start

xephyr.require:
    @# Assert Xephyr is available or fail with error message
    cmk.require.tool(Xephyr, Try installing package 'xserver-xephyr' to continue.)

xephyr.start:
    @# Start Xephyr in the background.
    Xephyr -ac -screen ${APP_DIMS} -br -reset -terminate ${APP_DISPLAY} &

gui: xephyr.init i3.stop i3.build i3.up.detach i3.exec/i3.start_apps 
    @# Main Entrypoint
    @# Start an XOrg server, rebuild the i3 container if necessary.
    @# Then start/detach i3 WM, and start a few apps inside.

i3.start_apps:
    @# Private helper, intended to run in container.
    @# Use I3's RPC tool to drive the display.  
    @# Runs in the container, so using tools unavailable to host is ok.
    i3-msg exec glxgears
    i3-msg exec "feh -B black --scale ./demos/data/droste.png"
    i3-msg exec xeyes

__main__:
    @# Print usage info and exit.
    @# Main entrypoint does not actually start the GUI
    @# because we don't want this running from main test-suite
    cmk.log(${red}USAGE: ${__file__} gui)

We've already seen what it looks like: Running the demo just opens xeyes, uses feh to display an image, and starts glxgears. These tools and the window manager itself run inside the i3 container, but the environment is also accessible from other containers, or from your host.

$ ./demos/cmk/xephyr.cmk 

To test one way mix in other components, you can try running something like* DISPLAY=:23 xeyes from the host.

Application Skeleton


This is more of a "skeleton" than anything with real meat on the bones. But what's accomplished here is all about quickly shipping a whole environment as an application, and several aspects of the skeleton are carefully chosen for convenient extension:

🎯 Lifecycle:
The way this launches Xephyr and i3, the xorg server depends on the window manager, and the window manager depends on the xorg server. In case either one exits, the other will as well. Despite multiple blocking components, launching is nonblocking, and doesn't require additional thought about piecewise shut down for backgrounded stuff. As with other demos, it's possible to zip this into a completely self-contained application, using the packaging features.
🎯 Scriptable:
See the usage of i3-msg exec ? Good support for RPC in i3 makes this very scriptable. If i3 is available on the host, you can drive the GUI from "outside" using a shared socket. And in addition to controlling the window-manager via make-targets, you can also have it run targets, which might represent other containers, etc. Since i3 has many language bindings5, you can also drive this thing via compose.mk polyglots.
🎯 Configurable:
The config for i3 and its status bar that's embedded above could easily be an external file, and might seem like an unnecessary distraction for a small demo. For actual use-cases this is pretty nice though, because it makes it easy to offload the complexity of customized application startup and shortcuts somewhere else, without creating all the code for that from scratch.

For further extensions and creating project-specific dashboards or IDEs, you might want to consider combinations of other tools that are small, fast and focused. Ideally tools that are themselves scriptable and embeddable, like yazi, surf, vim, etc.

XPra Demo


Rather than the "native app" look and feel in the Xephyr demo, another approach is to expose x11 via a webbrowser. The main benefit of this is that it doesn't require external packages or even x11 on the host since we dockerize the xorg server itself. Xpra has lots of options, and we won't cover all those details here; see instead the main docs 4.

DOOM


The traditional thing here is to run doom, which is definitely kind of pixelated at the best of times!, but is pretty useful for basic latency testing, mouse/keyboard events, etc. Although there are many ways to run doom in a browser.. this is something different: a dockerized x11 app that's now been webified.

$ ./demos/cmk/xpra_doom.cmk 

And here's the code:

Summary
#!/usr/bin/env -S ./compose.mk mk.interpret!
# Demonstrating GUI capabilities with dockerized XPra,
# plus a dockerized app, served via HTTP / HTML5 and 
# accessed via a host browser.
#
# USAGE: ./demos/cmk/xpra-doom.cmk serve

export DISPLAY=:10
export XPRA_PORT_INTERNAL?=14500
export XPRA_ARGS?=--mdns=no --printing=no --speaker=no --sharing=yes --audio=no --pulseaudio=no --webcam=no --microphone=off --html=on --daemon=no --window-close=disconnect --file-transfer=no --tray=no --system-tray=no --exit-with-children=yes --terminate-children=yes 
export XPRA_PORT_EXTERNAL?=9091
export XPRA_SOURCES=https://raw.githubusercontent.com/Xpra-org/xpra/master/packaging/repos/noble/xpra-lts.sources
export CMK_AT_EXIT_TARGETS=doom.stop

 gui.services
services:
  doom:
    build:
      context: .
      dockerfile_inline: |
        FROM ${IMG_UBUNTU_BASE:-ubuntu:noble}
        ENV DEBIAN_FRONTEND=noninteractive
        RUN apt-get update -qq 
        RUN apt-get install -qq -y wget apt-transport-https software-properties-common ca-certificates
        RUN wget -O "/usr/share/keyrings/xpra.asc" https://xpra.org/xpra.asc
        RUN wget -O "/etc/apt/sources.list.d/xpra.sources" ${XPRA_SOURCES}
        RUN apt-get update -qq 
        RUN apt-get install -qq -y xpra woof-doom
    command: >-
      xpra start ${DISPLAY} --bind-tcp=0.0.0.0:${XPRA_PORT_INTERNAL:-14500}
        ${XPRA_ARGS} --start-child /usr/games/boom
    ports:
      - "${XPRA_PORT_EXTERNAL:-9091}:${XPRA_PORT_INTERNAL:-14500}"
    environment:
      - DISPLAY=${DISPLAY}
    working_dir: /workspace
    volumes:
      - ${PWD}:/workspace
      - ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock


serve: doom.stop doom.build doom.up.detach flux.timeout/5/doom.logs
    @# Start an XOrg server, rebuild the i3 container if necessary.
    @# Then start/detach i3 WM, and start a few apps inside.
    cmk.io.user_exit(Serving: http://localhost:${XPRA_PORT_EXTERNAL})

__main__:
    @# Print usage info and exit.
    @# Main entrypoint does not actually start the GUI
    @# because we don't want this running from main test-suite
    cmk.log(${red}USAGE: ${__file__} serve)

This example uses the 'seamless' mode for xpra instead of the desktop mode.

Back to I3


With the doom example out of the way.. the structure of our earlier xephyr demo is more practical in terms of combining multiple apps, providing tiling geometry with i3, and shipping with enough structure so that it is easily configured and extended.

This example returns to i3 and adds a few variations: we'll drop the status-bar config from the previous example, and add usage of i3-layout 6 (very similar to the dwindle tool7 that handles spiral layouts with the embedded TUI).

$ ./demos/cmk/xpra.cmk gui 

By default this opens a webserver at http://localhost:9090. Note that server authentication is disabled by default for all these demos. See the XPra docs for setting user/password details. Here's the code:

Summary
#!/usr/bin/env -S ./compose.mk mk.interpret!
# Demonstrating GUI capabilities with XPra and i3, served via HTML5.
#
# USAGE: ./demos/cmk/xpra.cmk serve

export APP_DIMS?=1024x500
export APP_DISPLAY?=:23
export CMK_AT_EXIT_TARGETS=xpra.stop

export XPRA_ARGS?=--mdns=no --printing=no --speaker=no --sharing=yes --audio=no --pulseaudio=no --webcam=no --microphone=off --html=on --exit-with-children=yes --terminate-children=yes --daemon=no --window-close=disconnect --file-transfer=no --tray=no --system-tray=no --resize-display="1200x768"
export XPRA_PORT_INTERNAL?=14500
export XPRA_SOURCES=https://raw.githubusercontent.com/Xpra-org/xpra/master/packaging/repos/noble/xpra-lts.sources
export XPRA_PORT_EXTERNAL=9090

# Begin containerized version of i3 window manager and support files.
 gui.services
configs:
  # See https://i3wm.org/docs/userguide.html for a complete reference
  i3conf:
    content: |
      # Set mod key to Alt (Mod1)
      set $$mod Mod1
      font pango:Monaco 10
      # Use Mouse+$mod to drag floating windows
      floating_modifier $$mod
      # Kill focused window
      bindsym $$mod+Shift+q kill
      # Change focus between windows
      bindsym $$mod+h focus left
      bindsym $$mod+j focus down
      bindsym $$mod+k focus up
      bindsym $$mod+l focus right
      # Split window
      bindsym $$mod+v split v
      bindsym $$mod+b split h
      # Toggle fullscreen
      bindsym $$mod+f fullscreen toggle
      # Layout toggle (stacked, tabbed, default)
      bindsym $$mod+s layout stacking
      bindsym $$mod+w layout tabbed
      bindsym $$mod+e layout toggle split
      # Floating toggle
      bindsym $$mod+Shift+space floating toggle
      exec i3-layouts
      set $$i3l spiral
services:
  xpra:
    build:
      context: .
      dockerfile_inline: |
        FROM ubuntu:24.04
        ENV DEBIAN_FRONTEND=noninteractive
        RUN apt-get update -qq 
        RUN apt-get install -y make procps
        RUN apt-get install -y wget apt-transport-https \
          software-properties-common ca-certificates
        RUN wget -O "/usr/share/keyrings/xpra.asc" https://xpra.org/xpra.asc
        RUN wget -O "/etc/apt/sources.list.d/xpra.sources" ${XPRA_SOURCES}
        RUN apt-get update -qq 
        RUN apt-get install -qq -y xpra i3 i3status xterm x11-apps mesa-utils
        # Requires for i3-layouts 
        RUN apt-get install -qq -y python3-pip xdotool
        RUN pip3 install --break-system-packages i3-layouts
        # We'll volume-mount this later, swap out the default splash screen
        RUN rm /usr/share/xpra/www/background.png
    ports:
      - "${XPRA_PORT_EXTERNAL:-9090}:${XPRA_PORT_INTERNAL:-14500}"
    environment:
      - DISPLAY=:10
      - XDG_RUNTIME_DIR=/tmp
    command: >-
      xpra start-desktop :10 --bind-tcp=0.0.0.0:${XPRA_PORT_INTERNAL:-14500} \
        ${XPRA_ARGS} --start-child=i3 
    working_dir: /workspace
    volumes:
      - ${PWD}:/workspace
      - ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
      - ${PWD}/docs/img/icon.png:/usr/share/xpra/www/background.png
    configs:
      - source: i3conf
        target: /root/.config/i3/config


serve: xpra.stop xpra.build xpra.up.detach xpra.exec/i3.start_apps flux.timeout/5/xpra.logs
    @# Starts an XOrg server, rebuilding the xpra container if necessary.
    @# Then start/detach i3 WM, and start a few apps inside.  
    @# Tail the logs for a while, wait for user-input, exit cleanly.
    cmk.io.user_exit(Serving: http://localhost:${XPRA_PORT_EXTERNAL}\nPress any key to exit.)

i3.start_apps: io.wait/5
    @# Use I3's RPC tool to drive the display.  
    @# Runs in the container, so using tools unavailable to host is ok.
    i3-msg exec 'glxgears 2>&1 > /dev/null'
    i3-msg exec 'glxgears 2>&1 > /dev/null'
    i3-msg exec 'glxgears 2>&1 > /dev/null'
    i3-msg exec 'glxgears 2>&1 > /dev/null'
    i3l spiral

__main__:
    @# Print usage info and exit.
    @# Main entrypoint does not actually start the GUI
    @# because we don't want this running from tests.  
    cmk.log(${red}USAGE: ${__file__} serve)

Other differences with this demo are that we do not share the X11 socket with the host, so by default you cannot launch applications in this environment by simply exporting DISPLAY. Besides sharing the unix socket, it's also possible to use XPra from the host and share the TCP socket.

Besides auth, this example is rough in terms of proper daemonization, clean logging, and clean exits. Rather than backgrounding the server and hoping you remember to kill it, it runs in the foreground to make debugging changes easier. When you're done with the server, just press enter on the console to exit and you should see a clean exit. But note that Ctrl-C may not stop the server. You can use ./demos/cmk/xpra.cmk xpra.stop to ensure shutdown or use docker.stop.all to kill everything indiscriminately.

Hermetic GUIs


One last variation on XPra usage. The xephyr demo shares X11 sockets and leverages DISPLAY variables for the basic functionality, which is a pretty good level of isolation. The trouble is that it does explicitly require that Xephyr is available as a package on the host, and implicitly requires the host is already running x11. Meanwhile.. the XPra demos so far are working around both of these requirements, but now we have the new requirement for daemonization and port sharing.

You might wonder.. is this kind of breakdown in isolation strictly required? After all.. the embedded TUI which is a more "core" feature of compose.mk does manage to avoid servers and daemonization. For GUIs though, and setting aside X11.. most of the obvious transport options still have some kind local server in the middle10. Networking trickery with containers is a way around this, but then things like DNS and virtual addresses begin to complicate matters and make cross-platform harder.

The simplest thing that might work here reuses a trick from the notebooking demo, but does involve a tactical retreat back towards TUIs instead of GUIs. In the example below, we use a console-friendly browser (i.e. carbonyl 8) that can be dockerized, then use a private docker network between the X11 server and the browser to show the display. Running the example looks like this:

$ ./demos/cmk/xpra-net.cmk gui 

The corresponding code is below. Look, no open ports! We use xscreensaver9, running in xpra's "desktop" mode. Note also that rather than embedding carbonyl directly in the docker compose file, we use tux.browser and specify the network the container should join.

Summary
#!/usr/bin/env -S ./compose.mk mk.interpret!
# Demonstrating GUI capabilities with XPra and carbonyl, served via HTML5.
#
# USAGE: ./demos/cmk/xpra-net.cmk gui

export CMK_AT_EXIT_TARGETS=xscreensaver.stop
export XPRA_PORT_INTERNAL?=14500
export XPRA_ARGS?=--mdns=no --printing=no --speaker=no --sharing=yes --audio=no --pulseaudio=no --webcam=no --microphone=off --html=on --daemon=no --window-close=disconnect --file-transfer=no --tray=no --system-tray=no --exit-with-children=yes --terminate-children=yes
export XPRA_SOURCES=https://raw.githubusercontent.com/Xpra-org/xpra/master/packaging/repos/noble/xpra-lts.sources

 gui.services
services:
  xscreensaver:
    build:
      context: .
      dockerfile_inline: |
        FROM ubuntu:24.04
        ENV DEBIAN_FRONTEND=noninteractive
        RUN apt-get update -qq 
        RUN apt install -y wget apt-transport-https software-properties-common ca-certificates
        RUN wget -O "/usr/share/keyrings/xpra.asc" https://xpra.org/xpra.asc
        RUN wget -O "/etc/apt/sources.list.d/xpra.sources" ${XPRA_SOURCES}
        RUN apt-get update -qq
        RUN apt-get install -qq -y xpra xterm xscreensaver
    command: >-
      xpra start-desktop :10 --bind-tcp=0.0.0.0:${XPRA_PORT_INTERNAL} ${XPRA_ARGS} 
        --start-child '/usr/libexec/xscreensaver/moebiusgears -root'
    environment:
      - DISPLAY=:10
    working_dir: /workspace
    volumes:
      - ${PWD}:/workspace
      - ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
    networks:
      - app_network
networks:
  app_network:
    name: app_network


gui: xscreensaver.stop xscreensaver.build \
  xscreensaver.up.detach flux.timeout/4/xscreensaver.logs \
  app.open 

app.open:
    @# Opens the carbonyl browser in docker.
    @# This uses the `app_network` as defined in 
    @# the compose file above, and the service-name is the host name.
    url="http://xscreensaver:${XPRA_PORT_INTERNAL}" \
        net=app_network this.tux.browser

__main__:
    @# Print usage info and exit.
    @# Main entrypoint does not actually start the GUI
    @# because we don't want this running from main test-suite
    cmk.log(${red}USAGE: ${__file__} gui)

XQuartz Demo


Although the XPra demo is OSX friendly thanks to using a browser, we lost the "native application" aspect. For OSX, the only way to get this back is by using xquartz.

Coming soon, stay tuned for a demo. Meanwhile: this looks like a good resource, and this one too.

References



  1. https://github.com/mviereck/x11docker 

  2. https://freedesktop.org/wiki/Software/Xephyr/ 

  3. https://www.xquartz.org/ 

  4. https://xpra.org/index.html 

  5. For example https://github.com/altdesktop/i3ipc-python 

  6. https://github.com/eliep/i3-layouts 

  7. dwindle 

  8. fathyb/carbonyl 

  9. kaleidocycle @ xscreensaver screenshots 

  10. For example: (Browser → WebSocket → Local server → IPC / Unix socket). You can change the server/transport type (for example: HTTP → SSH), but eliminating it completely is hard.