LXD / Incus profile for GUI apps: Wayland, X11 and Pulseaudio

I made a profile for running GUI apps in an LXD / Incus container. It supports wayland, X11 and pulseaudio. This profile is based on work of Justin Ludwig, thank you!

Profile has been tested using:

  • both LXD and Incus
  • Ubuntu 22.04 as a host
  • Ubuntu 22.04 container images, both from official Canonical repository ubuntu:22.04 and community repository images:ubuntu/jammy/cloud

Note: those Ubuntu images have a default ubuntu user, which is hard-coded in the profile.

This post is divided into four parts:

  1. Profile
  2. Explanation
  3. Testing
  4. Troubleshooting

1. Profile

Note: profile adds some environment variables to .profile file inside container. Every time you change a profile using command lxc profile edit <profile_name> it will be applied once again to all containers using it and therefore those environment variables will be duplicated in .profile files. This doesn’t break anything, just be aware of that.

  raw.idmap: both 1000 1000
  security.nesting: "true"
  user.user-data: |
    package_update: true
    package_upgrade: true
    package_reboot_if_required: true
      - pulseaudio-utils
    - path: /usr/local/bin/mystartup.sh
      permissions: 0755
      content: |
        uid=$(id -u)
        mkdir -p $run_dir && chmod 700 $run_dir && chown $uid:$uid $run_dir
        ln -sf /mnt/.container_wayland_socket $run_dir/wayland-0
        mkdir -p $run_dir/pulse && chmod 700 $run_dir/pulse && chown $uid:$uid $run_dir/pulse
        ln -sf /mnt/.container_pulseaudio_socket $run_dir/pulse/native
        mkdir -p $tmp_dir
        ln -sf /mnt/.container_x11_socket $tmp_dir/X0
    - path: /usr/local/etc/mystartup.service
      content: |
    - mkdir -p /home/ubuntu/.config/systemd/user/default.target.wants
    - ln -s /usr/local/etc/mystartup.service /home/ubuntu/.config/systemd/user/default.target.wants/mystartup.service
    - ln -s /usr/local/etc/mystartup.service /home/ubuntu/.config/systemd/user/mystartup.service
    - chown -R ubuntu:ubuntu /home/ubuntu
    - echo 'export WAYLAND_DISPLAY=wayland-0' >> /home/ubuntu/.profile
    - echo 'export XDG_SESSION_TYPE=wayland' >> /home/ubuntu/.profile
    - echo 'export QT_QPA_PLATFORM=wayland' >> /home/ubuntu/.profile
    - echo 'export DISPLAY=:0' >> /home/ubuntu/.profile
description: GUI Wayland and X11 profile with pulseaudio
    type: gpu
    gid: 44
    source: /run/user/1000/wayland-0
    path: /mnt/.container_wayland_socket
    type: disk
    source: /tmp/.X11-unix/X0
    path: /mnt/.container_x11_socket
    type: disk
    source: /run/user/1000/pulse/native
    path: /mnt/.container_pulseaudio_socket
    type: disk

The easiest way to use a profile like that is to copy it into a text file, then create an empty profile in LXD:

lxc profile create <profile_name>

and update that profile with the file’s content:

lxc profile edit <profile_name> < /<path>/<file_name>

2. Explanation

For an in depth explanation of how this profile creates a script and startup systemd service that links all sockets to their usual location inside the container, please read Justin’s post at his blog. Here I’ll explain only tweaks I made:

  • Installing pulseaudio-utils package will create pulseaudio cookie inside container.
  • I added X11 socket for apps that don’t use Wayland yet.
  • All sockets are shared as type: disk, not type: proxy device.
  • Adding GPU with gid: 44 enables GPU hardware video acceleration in containers. See Testing section below.
  • Key security.nesting: "true" is for Steam, Docker, etc.

3. Testing

Launch container from either source using this profile:

lxc launch ubuntu:22.04 -p default -p <profile_name> <container_name>
lxc launch images:ubuntu/jammy/cloud -p default -p <profile_name> <container_name>

Now login into your container:

lxc exec <container_name> -- sudo --user ubuntu --login

Pulseaudio socket is called native and you can find it at /run/user/1000/pulse/. Wayland socket wayland-0 is in /run/user/1000/ folder, and X11 socket X0 is in /tmp/.X11-unix/ folder. On host in /tmp/.X11-unix/ folder you will find also Xwayland socket as X1.

All those sockets inside container should be visible at /mnt/ and linked into proper folders. Run those command to check if that’s true:

ll /mnt/.container*
ll /tmp/.X11-unix/X?
ll /run/user/*/wayland-?
ll /run/user/*/pulse/native

Check if most important environment variables are set, mainly WAYLAND_DISPLAY=wayland-0 and DISPLAY=:0:

printenv | grep -i display

Check if your user inside container is part of the video group using groups command. Then see if video render is owned by this group using ll /dev/dri/ command. Output should show root video (without gid: 44 it would be root root):

crw-rw---- 1 root video 226,   0 Nov 15 08:41 card0
crw-rw---- 1 root video 226, 128 Nov 15 08:41 renderD128

To test pulseaudio run pactl info command couple of times. If it shows Connection failure: Access denied at least once, then see Troubleshooting section.

For a final test, install Chrome, then run it in Wayland or X11 mode and watch any Youtube video with sound:

sudo apt update && sudo apt upgrade -y
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo apt install ~/google-chrome-stable_current_amd64.deb
sudo apt install libegl1

Without libegl1 package, Chrome will complain. Depending on which ubuntu image you used to create the container, Chrome will spill out some errors, but should work perfectly fine. To run Chrome in Wayland mode, use this command:

google-chrome --enable-features=UseOzonePlatform --ozone-platform=wayland

To run it in X11 mode, simply use this command:


Now on the host you can run xlsclients and see if Chrome shows up when run in X11 mode and if it’s absent when run in Wayland mode.

4. Troubleshooting

If pulseaudio test pactl info showed Connection failure: Access denied, then try copying pulseaudio cookie from host ~/.config/pulse/cookie to container:

lxc file push -p --mode=600 --gid=1000 --uid=1000 ~/.config/pulse/cookie <container_name>/home/ubuntu/.config/pulse/

If you still have problems with pulseaudio you may try to disable shared memory inside container in /etc/pulse/client.conf config file manually or with this command:

sed -i "s/; enable-shm = yes/enable-shm = no/g" /etc/pulse/client.conf

If Chrome doesn’t start in X11 mode, you can try changing in profile socket X0 to X1 and corresponding environment variable from DISPLAY=:0 to DISPLAY=:1 .


Don’t work (( When I setted raw.idmap='both 1000 1000' to container I got error when start container:


lxc websurf 20240229071556.860 ERROR    conf - ../src/lxc/conf.c:lxc_map_ids:3701 - newuidmap failed to write mapping "newuidmap: uid range [1000-1001) -> [1000-1001) not allowed": newuidmap 52593 0 1000000 1000 1000 1000 1 1001 1001001 64535
lxc websurf 20240229071556.860 ERROR    start - ../src/lxc/start.c:lxc_spawn:1788 - Failed to set up id mapping.
lxc websurf 20240229071556.860 ERROR    lxccontainer - ../src/lxc/lxccontainer.c:wait_on_daemonized_start:878 - Received container state "ABORTING" instead of "RUNNING"
lxc websurf 20240229071556.861 ERROR    start - ../src/lxc/start.c:__lxc_start:2107 - Failed to spawn container "websurf"
lxc websurf 20240229071556.861 WARN     start - ../src/lxc/start.c:lxc_abort:1036 - No such process - Failed to send SIGKILL via pidfd 17 for process 52593
lxc 20240229071556.885 ERROR    af_unix - ../src/lxc/af_unix.c:lxc_abstract_unix_recv_fds_iov:218 - Connection reset by peer - Failed to receive response
lxc 20240229071556.885 ERROR    commands - ../src/lxc/commands.c:lxc_cmd_rsp_recv_fds:128 - Failed to receive file descriptors for command "get_init_pid"

This profile assumes that your user on the host has a UID and GID 1000, as well as the user in the container has a UID and GID 1000. You can check this using the id -u and id -g commands. Change the values according to your system.

raw.idmap='both 1000 1000'


raw.idmap='both <host_user_uid_and_gid> <container_user_uid_and_gid>'

Did that help?

Yes, I added specific idmap to /etc/subuid and /etc/subgid:


But now I got next an error when launch glxinfo inside my container:

ubuntu@websurf:~$ glxinfo
Authorization required, but no authorization protocol specified

Error: unable to open display :0

The container have DISPLAY env variable:

ubuntu@websurf:~$ echo $DISPLAY

and x11 socket:

ubuntu@websurf:~$ ls -la /tmp/.X11-unix/

lrwxrwxrwx 1 root root  14 Mar  8 08:44 X0 -> /mnt/.container_x11_socket

You can pass X11 socket as proxy device instead of disk device. Just delete from mystartup.sh those lines:

        mkdir -p $tmp_dir
        ln -sf /mnt/.container_x11_socket $tmp_dir/X0

And change x11_socket: to:

    type: proxy
    bind: container
    security.gid: "1000"
    security.uid: "1000"
    connect: unix:@/tmp/.X11-unix/X0
    listen: unix:@/tmp/.X11-unix/X0

Or use my new profile, where scripts are executed by cloud-init automatically :

1 Like