Sandbox GUI apps like Antigravity or other Agentic Editors in an LXC container while rendering them natively on your host desktop.
Containers are great for isolating workloads — until you want to run something with a GUI. At that point, you’re usually told to just use a VM and move on. But there’s a cleaner way: proxy your host’s display sockets into an LXC container, give it GPU access, and let it render like any other desktop app.
This post walks through an LXD profile that does exactly that. I use it to run applications like Antigravity IDE in a sandboxed container, isolated from my host system but rendered natively on my desktop.
On a modern Linux desktop, graphical apps talk to the compositor over Unix domain sockets — either Wayland (/run/user/1000/wayland-0) or X11 (/tmp/.X11-unix/X0). These sockets live on the host filesystem. A container, by default, has no access to them.
There are a few common workarounds:
The cleanest approach is LXD’s proxy device, which sets up a relay between a path on the host and a path inside the container. The container gets its own socket endpoint. Nothing host-side is directly exposed.
config:
security.nesting: "true"
cloud-init.user-data: |
#cloud-config
package_update: true
packages:
# Desktop integration and session management
- dbus-user-session
- xdg-utils
- x11-utils
# Core graphics and hardware acceleration drivers
- libgl1
- libgl1-mesa-dri
- libegl1
- mesa-vulkan-drivers
# Wayland rendering base
- libwayland-egl1
# Base typography (prevents invisible text in headless containers)
- fontconfig
- fonts-liberation
- fonts-ubuntu
write_files:
- path: /usr/local/bin/setup-gui-sockets.sh
permissions: "0755"
content: |
#!/bin/bash
uid=1000
gid="$(getent passwd ${uid} | cut -d: -f4)"
run_dir="/run/user/${uid}"
tmp_dir="/tmp/.X11-unix"
mkdir -p "${run_dir}" && chmod 700 "${run_dir}" && chown ${uid}:${gid} "${run_dir}"
mkdir -p "${tmp_dir}" && chmod 1777 "${tmp_dir}"
[ -S /mnt/wayland-0 ] && [ ! -e "${run_dir}/wayland-0" ] && ln -s /mnt/wayland-0 "${run_dir}/wayland-0"
[ -S /mnt/X1 ] && [ ! -e "${tmp_dir}/X1" ] && ln -s /mnt/X1 "${tmp_dir}/X1"
- path: /etc/systemd/system/setup-gui-sockets.service
content: |
[Unit]
Description=Link GUI sockets from /mnt to runtime dirs
After=local-fs.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/setup-gui-sockets.sh
[Install]
WantedBy=multi-user.target
- path: /home/ubuntu/.profile
append: true
owner: "ubuntu:ubuntu"
defer: true
content: |
export WAYLAND_DISPLAY=wayland-0
export XDG_SESSION_TYPE=wayland
export QT_QPA_PLATFORM=wayland
export DISPLAY=:1
export XDG_RUNTIME_DIR=/run/user/1000
runcmd:
- systemctl daemon-reload
- systemctl enable --now setup-gui-sockets.service
- usermod -a -G video,render ubuntu
description: Sandboxed container for Graphical Applications
devices:
gpu:
type: gpu
gid: 44
wayland_socket:
type: proxy
bind: container
connect: unix:/run/user/1000/wayland-0
listen: unix:/mnt/wayland-0
security.uid: "1000"
security.gid: "1000"
mode: "0777"
xwayland_socket:
type: proxy
bind: container
connect: unix:/tmp/.X11-unix/X1
listen: unix:/mnt/X1
security.uid: "1000"
security.gid: "1000"
mode: "0777"The devices section is where the real work happens.
wayland_socket and xwayland_socket are both proxy type devices. LXD proxies incoming connections on listen (inside the container, at /mnt/wayland-0 and /mnt/X1) to connect (on the host, at the actual socket paths). bind: container means the listening end is created inside the container rather than on the host.
security.uid and security.gid: "1000" ensure the proxy process runs as UID/GID 1000, matching the ubuntu user in the container. Without this, socket permissions become a wall of EACCES.
Why both Wayland and X11? Some apps (especially Electron-based tools like Antigravity) may fall back to X11 via XWayland even on a Wayland session. Providing both sockets means you don’t have to fight with environment variables to coerce an app into the right protocol.
gpu passes through the host GPU. The gid: 44 maps to the render group on most Debian/Ubuntu systems, giving the container access to /dev/dri/renderD128 for hardware-accelerated rendering. Software rendering works too, but is noticeably slower for GPU-composited UIs.
The packages install three things:
libgl1, libgl1-mesa-dri, libegl1, mesa-vulkan-drivers, and libwayland-egl1 give the container a complete software and hardware rendering path. Without these, apps either crash on startup or fall back to CPU rendering.dbus-user-session and xdg-utils handle inter-process messaging and MIME/URL dispatch. Many apps assume a D-Bus session is running; without it, features silently break.fonts-liberation and fonts-ubuntu cover the most common font families that apps expect to find.The proxy devices create sockets at /mnt/wayland-0 and /mnt/X1 inside the container. But applications don’t look there — they look at the canonical paths: $XDG_RUNTIME_DIR/wayland-0 and /tmp/.X11-unix/X*.
The setup-gui-sockets.sh script bridges this gap by symlinking the /mnt sockets into the expected locations:
[ -S /mnt/wayland-0 ] && [ ! -e "${run_dir}/wayland-0" ] && ln -s /mnt/wayland-0 "${run_dir}/wayland-0"
[ -S /mnt/X1 ] && [ ! -e "${tmp_dir}/X1" ] && ln -s /mnt/X1 "${tmp_dir}/X1"It also creates /run/user/1000 with the correct permissions (700, owned by ubuntu). This directory must exist before most Wayland-aware apps will start — many check for it at launch and bail if it’s missing or has the wrong ownership.
The script runs as a oneshot systemd service triggered on local-fs.target, so it executes early in boot before any user session starts.
The .profile additions wire up the session for both Wayland and X11:
export WAYLAND_DISPLAY=wayland-0 # Wayland socket name (relative to XDG_RUNTIME_DIR)
export XDG_SESSION_TYPE=wayland # Tells toolkits this is a Wayland session
export QT_QPA_PLATFORM=wayland # Forces Qt apps to use the Wayland backend
export DISPLAY=:1 # X display number (matches XWayland socket X1)
export XDG_RUNTIME_DIR=/run/user/1000 # Runtime dir for the ubuntu userThe DISPLAY=:1 matches the X1 socket rather than the more common :0, since :0 is typically the host’s own X session. Using :1 avoids collisions if XWayland is already running on display zero.
Save the profile to a file (gui.yaml), then:
cat gui.yaml | lxc profile create gui -Launch a container with it:
lxc launch ubuntu:24.04 ai-dev --profile default --profile guiAfter cloud-init finishes (watch with lxc exec ai-dev -- cloud-init status --wait), shell in and install your app:
lxc shell ai-dev
# install Antigravity, run it — it renders on your host desktopwl-paste/wl-copy bridge or a compositor-level solution.gpu device and group IDs may need adjustment. NVIDIA drivers aren’t Mesa, so the render group and device paths differ.ubuntu user. If your host and container UIDs differ, the socket security.uid values need to match.