Introduction
Recently I encountered a situation where I was running code-server inside an Incus container as a remote development environment, while working on an application that included a graphical user interface. Although the development workflow itself worked well, testing and developing the GUI component was not possible directly inside the container. The challenge was enabling GUI applications running in the Incus container to display on the host system, which provides the desktop environment.
So this tutorial demonstrates how to run GUI applications inside an LXC/Incus container and display their user interface on the host system, while still keeping the application fully isolated within the container.
Step 1: Ensure that the host runs X11
Since Wayland is not reliably compatible with all the features of GUI and graphics frameworks such as Qt, JavaFX, or LWJGL, ensure that you run X11 instead of Wayland.
On the host, type
echo $XDG_SESSION_TYPEThis command has to return
x11If it returns wayland, log out and select Ubuntu on Xorg at the login screen.
Step 2: Setup container-to-X-server connections
To allow local containers to access the X server, you have to adjust the access policy on the host. We’ll use the session profile config to make this permanent.
nano ~/.profileAppend this to the end of your ~/.profile file:
# X11: Allow connections from local containers
xhost +local:Next, mount the host’s X11 socket into your target container. For that we’ll use a disk device mount within LXC/Incus.
EDITOR=nano incus config edit <YOURCONTAINER>Here the mount is called mount-x11, but you can call it whatever you want really.
devices:
mount-x11:
path: /tmp/.X11-unix
readonly: "true"
source: /tmp/.X11-unix
type: diskNow restart your container to apply this change.
incus restart <YOURCONTAINER>This completes all the necessary steps on the host side of things. Next, we move on to configuring the container.
Step 3: Setup the container itself
Change into your container with
incus exec <YOURCONTAINER> bashMake sure to set required environment variables. Instead of ~/.profile, we’ll use ~/.bashrc now, as ~/.profile triggers at login shells, which our container does not have.
nano ~/.bashrcAppend this to the end of your ~/.bashrc file:
# X11: Set environment variables for host desktop access
export DISPLAY=:1
export XDG_RUNTIME_DIR=/tmp/runtime-$(id -u)Save your changes and proceed to install these packages:
apt-get install libx11-6 libxext6 libxrender1 libxtst6 libxi6 mesa-utils libgl1Restart your container and log back into it.
shutdown -r now
incus exec <YOURCONTAINER> bashStep 4: Testing it out
Now you can test if everything worked out. Here’s the filtered result of GLXInfo for OpenGL for example:
glxinfo | grep OpenGL
OpenGL vendor string: Mesa
OpenGL renderer string: llvmpipe (LLVM 20.1.2, 256 bits)
OpenGL core profile version string: 4.5 (Core Profile) Mesa 25.0.7-0ubuntu0.24.04.2
OpenGL core profile shading language version string: 4.50
OpenGL core profile context flags: (none)
OpenGL core profile profile mask: core profile
OpenGL core profile extensions:
OpenGL version string: 4.5 (Compatibility Profile) Mesa 25.0.7-0ubuntu0.24.04.2
OpenGL shading language version string: 4.50
OpenGL context flags: (none)
OpenGL profile mask: compatibility profile
OpenGL extensions:
OpenGL ES profile version string: OpenGL ES 3.2 Mesa 25.0.7-0ubuntu0.24.04.2
OpenGL ES profile shading language version string: OpenGL ES GLSL ES 3.20
OpenGL ES profile extensions:Or run GLXGears to actually see an OpenGL rendered application window.
glxgears