Skip to content

Short setups to provide X display to container

mviereck edited this page Apr 29, 2021 · 19 revisions

Sharing host X display for single applications

This is similar to x11docker option --hostdisplay:

  • Share access to host X server with environment variable DISPLAY and X unix socket in /tmp/.X11-unix.
  • Allow access with xhost for current local user and create a similar container user.
  • Allow shared memory with --ipc=host to avoid RAM access failures and rendering glitches due to X extension MIT-SHM.
  • Create container user similar to host user to avoid root in container with --user $(id -u):$(id -g).
  • Drop all capabilities with --cap-drop=ALL --security-opt=no-new-privileges to improve container security.
  • Warning: This nice short solution has the disadvantage of breaking container isolation. X security leaks like keylogging and remote host control can be abused by container applications.
xhost +SI:localuser:$(id -un)
docker run --rm \
            -e DISPLAY=$DISPLAY \
            -v /tmp/.X11-unix:/tmp/.X11-unix:rw \
            --ipc=host \
            --user $(id -u):$(id -g) \
            --cap-drop=ALL \
            --security-opt=no-new-privileges \
            IMAGENAME IMAGECOMMAND

Providing nested X server Xephyr

This is similar to x11docker option --xephyr and avoids the above security leaks:

  • Run Xephyr with disabled shared memory (X extension MIT-SHM) and disabled X extension XTEST.
  • Set DISPLAY and share access to new unix socket /tmp/.X11-unix/X1.
  • Create container user similar to host user to avoid root in container with --user $(id -u):$(id -g).
  • Drop all capabilities with --cap-drop=ALL --security-opt=no-new-privileges to improve container security.
Displaynumber=1
Xephyr :$Displaynumber -extension MIT-SHM -extension XTEST &
docker run --rm \
            -e DISPLAY=:$Displaynumber \
            -v /tmp/.X11-unix/X$Displaynumber:/tmp/.X11-unix/X$Displaynumber:rw \
            --user $(id -u):$(id -g) \
            --cap-drop=ALL \
            --security-opt=no-new-privileges \
            IMAGENAME IMAGECOMMAND

This solution is more secure than the above one as it does not give access to display :0 with host applications and does not need --ipc=host. To use this with single applications you can run a host window manager on Xephyr display, too, for example with env DISPLAY=:1 x-window-manager.

Extended Xephyr script

  • Finds free display number and creates an X cookie.
  • Unprivileged container user with entry in /etc/passwd of container.
  • Restricted container capabilities to improve security.
  • A window manager from host can be specified.
  • Examples:
    • xephyrdocker : x11docker/xfce
    • xephyrdocker "openbox --sm-disable" x11docker/lxde pcmanfm
    • xephyrdocker xfwm4 --device /dev/snd jess/nes /games/zelda.rom
#! /bin/bash
#
# Xephyrdocker:     Example script to run docker GUI applications in Xephyr.
#
# Usage:
#   Xephyrdocker WINDOWMANAGER DOCKERIMAGE [IMAGECOMMAND [ARGS]]
#
# WINDOWMANAGER     host window manager for use with single GUI applications.
#                   To run without window manager from host, use ":"
# DOCKERIMAGE       docker image containing GUI applications or a desktop
# IMAGECOMMAND      command to run in image
#
Windowmanager="$1" && shift
Dockerimage="$*"

# Container user
Useruid=$(id -u)
Usergid=$(id -g)
Username="$(id -un)"
[ "$Useruid" = "0" ] && Useruid=1000 && Usergid=1000 && Username="user$Useruid"

# Find free display number
for ((Newdisplaynumber=1 ; Newdisplaynumber <= 100 ; Newdisplaynumber++)) ; do
  [ -e /tmp/.X11-unix/X$Newdisplaynumber ] || break
done
Newxsocket=/tmp/.X11-unix/X$Newdisplaynumber

# Cache folder and files
Cachefolder=/tmp/Xephyrdocker_X$Newdisplaynumber
[ -e "$Cachefolder" ] && rm -R "$Cachefolder"
mkdir -p $Cachefolder
Xclientcookie=$Cachefolder/Xcookie.client
Xservercookie=$Cachefolder/Xcookie.server
Xinitrc=$Cachefolder/xinitrc
Etcpasswd=$Cachefolder/passwd

# Command to run docker
# --rm                               Created container will be discarded.
# -e DISPLAY=$Newdisplay             Set environment variable to new display.
# -e XAUTHORITY=/Xcookie             Set environment variable XAUTHORITY to provided cookie.
# -v $Xclientcookie:/Xcookie:ro      Provide cookie file to container.
# -v $NewXsocket:$NewXsocket:ro      Share new X socket of Xephyr.
# --user $Useruid:$Usergid           Security: avoid root in container.
# -v $Etcpasswd:/etc/passwd:ro       Share custom /etc/passwd file with user entry.
# --group-add audio                  Allow ALSA access to /dev/snd if shared with '--device /dev/snd' .
# --cap-drop ALL                     Security: disable needless capabilities.
# --security-opt no-new-privileges   Security: forbid new privileges.
# --init                             Run with init system tini if available.
Dockercommand="docker run --rm \
  -e DISPLAY=:$Newdisplaynumber \
  -e XAUTHORITY=/Xcookie \
  -v $Xclientcookie:/Xcookie:ro \
  -v $Newxsocket:$Newxsocket:rw \
  --user $Useruid:$Usergid \
  -v $Etcpasswd:/etc/passwd:ro \
  --group-add audio \
  --env HOME=/tmp \
  --cap-drop ALL \
  --security-opt no-new-privileges \
  $(command -v docker-init >/dev/null && echo --init) \
  $Dockerimage"

echo "docker command: 
$Dockercommand
"

# Command to run Xorg or Xephyr
#  /usr/bin/Xephyr                An absolute path to X server executable must be given for xinit.
#  :$Newdisplaynumber             First argument has to be new display.
#  -auth $Xservercookie           Path to cookie file for X server.
#  -extension MIT-SHM             Disable MIT-SHM to avoid rendering glitches and bad RAM access.
#  -nolisten tcp                  Disable TCP connections, local access only.
#  -retro                         Nice retro look.
Xcommand="/usr/bin/Xephyr :$Newdisplaynumber \
  -auth $Xservercookie \
  -extension MIT-SHM \
  -nolisten tcp \
  -screen 1000x750x24 \
  -retro"
  
echo "X server command:
$Xcommand
"

# Create /etc/passwd with unprivileged user
echo "root:x:0:0:root:/root:/bin/sh" >$Etcpasswd
echo "$Username:x:$Useruid:$Usergid:$Username,,,:/tmp:/bin/sh" >> $Etcpasswd

# Create xinitrc
{ echo "#! /bin/bash"

  echo "# set environment variables to new display and new cookie"
  echo "export DISPLAY=:$Newdisplaynumber"
  echo "export XAUTHORITY=$Xclientcookie"

  echo "# same keyboard layout as on host"
  echo "echo '$(setxkbmap -display $DISPLAY -print)' | xkbcomp - :$Newdisplaynumber"

  echo "# create new XAUTHORITY cookie file" 
  echo ":> $Xclientcookie"
  echo "xauth add :$Newdisplaynumber . $(mcookie)"
  echo "# create prepared cookie with localhost identification disabled by ffff,"
  echo "# needed if X socket is shared instead connecting over tcp. ffff means 'familiy wild'"
  echo 'Cookie=$(xauth nlist '":$Newdisplaynumber | sed -e 's/^..../ffff/')" 
  echo 'echo $Cookie | xauth -f '$Xclientcookie' nmerge -'
  echo "cp $Xclientcookie $Xservercookie"
  echo "chmod 644 $Xclientcookie"

  echo "# run window manager in Xephyr"
  echo $Windowmanager' & Windowmanagerpid=$!'

  echo "# show docker log"
  echo 'tail --retry -n +1 -F '$Dockerlogfile' 2>/dev/null & Tailpid=$!'

  echo "# run docker"
  echo "$Dockercommand"
} > $Xinitrc

xinit  $Xinitrc -- $Xcommand
rm -Rf $Cachefolder

Other operating systems

MS Windows

x11docker uses VcXsrv to provide an X display. Some day I'll write a short how-to on how to connect to VcXsrv with X over IP without x11docker.

macOS

A blogpost about providing XQuartz to docker containers. I did not test it, just providing this link as a pointer to a possible solution.

Clone this wiki locally