Run X application in a Docker container reliably on a server connected via SSH without "--net host"

Ruben picture Ruben · Jan 12, 2018 · Viewed 11.5k times · Source

Without a Docker container, it is straightforward to run an X11 program on a remote server using the SSH X11 forwarding (ssh -X). I have tried to get the same thing working when the application runs inside a Docker container on a server. When SSH-ing into a server with the -X option, an X11 tunnel is set up and the environment variable "$DISPLAY" is automatically set to typically "localhost:10.0" or similar. If I simply try to run an X application in a Docker, I get this error:

Error: GDK_BACKEND does not match available displays

My first idea was to actually pass the $DISPLAY into the container with the "-e" option like this:

docker run -ti -e DISPLAY=$DISPLAY name_of_docker_image

This helps, but it does not solve the issue. The error message changes to:

Unable to init server: Broadway display type not supported: localhost:10.0
Error: cannot open display: localhost:10.0

After searching the web, I figured out that I could do some xauth magic to fix the authentication. I added the following:

SOCK=/tmp/.X11-unix
XAUTH=/tmp/.docker.xauth
xauth nlist $DISPLAY | sed -e 's/^..../ffff/' | xauth -f $XAUTH nmerge -
chmod 777 $XAUTH
docker run -ti -e DISPLAY=$DISPLAY -v $XSOCK:$XSOCK -v $XAUTH:$XAUTH \ 
  -e XAUTHORITY=$XAUTH name_of_docker_image

However, this only works if also add "--net host" to the docker command:

docker run -ti -e DISPLAY=$DISPLAY -v $XSOCK:$XSOCK -v $XAUTH:$XAUTH \ 
  -e XAUTHORITY=$XAUTH --net host name_of_docker_image

This is not desirable since it makes the whole host network visible for the container.

What is now missing in order to get it fully to run on a remote server in a docker without "--net host"?

Answer

Ruben picture Ruben · Jan 13, 2018

I figured it out. When you are connecting to a computer with SSH and using X11 forwarding, /tmp/.X11-unix is not used for the X communication and the part related to $XSOCK is unnecessary.

Any X application rather uses the hostname in $DISPLAY, typically "localhost" and connects using TCP. This is then tunneled back to the SSH client. When using "--net host" for the Docker, "localhost" will be the same for the Docker container as for the Docker host, and therefore it will work fine.

When not specifying "--net host", the Docker is using the default bridge network mode. This means that "localhost" means something else inside the container than for the host, and X applications inside the container will not be able to see the X server by referring to "localhost". So in order to solve this, one would have to replace "localhost" with the actual IP-address of the host. This is usually "172.17.0.1" or similar. Check "ip addr" for the "docker0" interface.

This can be done with a sed replacement:

DISPLAY=`echo $DISPLAY | sed 's/^[^:]*\(.*\)/172.17.0.1\1/'`

Additionally, the SSH server is commonly not configured to accept remote connections to this X11 tunnel. This must then be changed by editing /etc/ssh/sshd_config (at least in Debian) and setting:

X11UseLocalhost no

and then restart the SSH server, and re-login to the server with "ssh -X".

This is almost it, but there is one complication left. If any firewall is running on the Docker host, the TCP port associated with the X11-tunnel must be opened. The port number is the number between the : and the . in $DISPLAY added to 6000.

To get the TCP port number, you can run:

X11PORT=`echo $DISPLAY | sed 's/^[^:]*:\([^\.]\+\).*/\1/'`
TCPPORT=`expr 6000 + $X11PORT`

Then (if using ufw as firewall), open up this port for the Docker containers in the 172.17.0.0 subnet:

ufw allow from 172.17.0.0/16 to any port $TCPPORT proto tcp

All the commands together can be put into a script:

XSOCK=/tmp/.X11-unix
XAUTH=/tmp/.docker.xauth
xauth nlist $DISPLAY | sed -e 's/^..../ffff/' | sudo xauth -f $XAUTH nmerge -
sudo chmod 777 $XAUTH
X11PORT=`echo $DISPLAY | sed 's/^[^:]*:\([^\.]\+\).*/\1/'`
TCPPORT=`expr 6000 + $X11PORT`
sudo ufw allow from 172.17.0.0/16 to any port $TCPPORT proto tcp 
DISPLAY=`echo $DISPLAY | sed 's/^[^:]*\(.*\)/172.17.0.1\1/'`
sudo docker run -ti --rm -e DISPLAY=$DISPLAY -v $XAUTH:$XAUTH \
   -e XAUTHORITY=$XAUTH name_of_docker_image

Assuming you are not root and therefore need to use sudo.

Instead of sudo chmod 777 $XAUTH, you could run:

sudo chown my_docker_container_user $XAUTH
sudo chmod 600 $XAUTH

to prevent other users on the server from also being able to access the X server if they know what you have created the /tmp/.docker.auth file for.

I hope this should make it properly work for most scenarios.