run gui applications in docker
it is possible to run gui applications in docker containers, at least on linux;
this problem mainly consists of two subproblems: connection and authentication;
x server connection
x uses a client-server architecture; the x server on host listens on a socket; gui applications on guest must connect to this x server socket to draw windows on host display;
depending on x server configuration, this socket can be one of these types:
-
an abstract unix domain socket: usually
@/tmp/.X11-unix/X0
; -
a pathname unix domain socket: usually
/tmp/.X11-unix/X0
; -
a tcp socket; usually disabled with
--nolisten tcp
;
to see what this socket really is, run on host:
$ netstat -npl | grep Xorg
on most personal computers, this would be a unix domain socket; to be precise, there might be both abstract and pathname unix domain sockets, and we can use either of them;
use pathname socket
we do pathname socket first, because this is simpler; to make this socket file accessible on guest, we can bind mount it onto guest;
the docker run
command has a -v, --volume
option to do this:
$ docker run -v /tmp/.X11-unix/X0:/tmp/.X11-unix/X0 ...
use abstract socket
abstract sockets are harder to deal with; man 7 network_namespaces
points out
the most important thing:
In addition, network namespaces isolate the UNIX domain abstract socket namespace (see unix(7)).
this means, guest and host cannot communicate via abstract sockets when they are located in distinct network namespaces (which is the normal case); to facilitate such communications, we have 2 solutions:
-
use
host
network driver:$ docker run --net=host ...
the
host
network driver puts guest and host in the same network namespace; -
use
bridge
network driver, and relay abstract sockets via tcp sockets;the
bridge
network driver puts each guest in its own network namespace but allows guest-guest and host-guest communication via veth pairs; sadly, these veth pairs do not help unix domain sockets;but we can help ourselves; specifically, we run one
socat
instance on host and one on guest:-
on host:
$ socat TCP-LISTEN:<port>,reuseaddr,fork,bind=<host> ABSTRACT-CONNECT:/tmp/.X11-unix/X0
-
on guest:
$ socat ABSTRACT-LISTEN:/tmp/.X11-unix/X0,fork TCP-CONNECT:<host>:<port>
in both commands:
-
<host>
is the ip address of network bridgedocker0
; we can find this address by running on host:$ ip addr show dev docker0
-
<port>
is an arbitrary port number (>=1024);
together, these commands establish a tunnel between
@/tmp/.X11-unix/X0
on host and@/tmp/.X11-unix/X0
on guest, leveraging available network stacks; -
x server authentication
now we have the connection from gui applications on guest to x server socket on host; next topic is authentication: such gui applications must be authenticated to use the x server on host;
most x servers have two ways of authentication:
-
xhost
: host list mechanism; -
xauth
: magic cookie mechanism;
we can use either way to authenticate; xhost
is simpler, but xauth
is more
secure; we can, but do not have to, use both at the same time;
xhost
authentication
xhost
controls access to x server using a host-based mechanism; when we run
command:
$ xhost
the output usually contains a line (<user>
is a user name):
SI:localuser:<user>
this line allows connection to x server by local user <user>
;
assuming we are running gui applications on guest with user root
; we need to
tell x server on host to accept connections coming from user root
; to do so,
we run this command on host:
$ xhost +SI:localuser:root
now if we run this again:
$ xhost
we will find another line:
SI:localuser:root
this line allows connection to x server by local user root
;
note that we are taking another assumption here: user namespaces are not being
isolated; see userns-remap for details; in short, this means the root
user
on guest is the same root
user on host; should this not be the case, we need
to find out the real user on host then amend the above command accordingly;
if we would rather grant access to all local connections, we can run:
$ xhost +local:
as a last resort, we can disable access control to grant access to everyone by running:
$ xhost +
but this is obviously not recommended;
xauth
authentication
this works like a browser cookie: a connection is allowed if the client proves knowledge of a secret; such secret is called a magic cookie;
xauth
is the tool to edit and display magic cookies:
-
display
xauth
information (authorization file, etc.):$ xauth info
-
display all magic cookies:
$ xauth [n]list
-
extract magic cookies for specified displays to file:
$ xauth [n]extract filename displayname
-
merge magic cookies from file:
$ xauth [n]merge filename
the authorization file serves as the magic cookie database; this file can be set
through command-line option, environment variable or default to ~/.Xauthority
;
create this file empty if it does not exist yet; doing so helps avoid some error
messages;
some xauth
commands have [n]
versions; these are useful when their input and
output need to be transmitted in non-binary form, such as during copy-paste;
to authenticate connection from guest to host, we first extract magic cookies on host then merge them on guest;
set x display name
gui applications need to know which x display to use; this is controlled by an
environment variable DISPLAY
; this environment variable should have been set
on host, so we just need to propagate it to guest:
$ docker run -e "DISPLAY" ...
examples
after these long explanations, we can finally put things together and give some
working examples; these examples are done with a container started from ubuntu
image, as root
user; when they run successfully, we would see rolling eyes in
a small window;
when authentication is needed, these examples prefer xauth
to xhost
;
some examples have <copy>
and <paste>
commands; these are not real commands
but places where manual intervetions are necessary; basically, we need to write
down the data on <copy>
and provide the same data on <paste>
; if we can ssh
from host into guest, then we can save some copy-paste by running this on host:
$ xauth extract - "$DISPLAY" | ssh <guest> xauth merge -
pathname socket
-
on host:
$ xauth nextract - "$DISPLAY" | <copy> $ docker run -it -v /tmp/.X11-unix/X0:/tmp/.X11-unix/X0 -e "DISPLAY" ubuntu bash
-
on guest:
$ apt-get -y update $ apt-get -y install xauth x11-apps $ touch ~/.Xauthority $ <paste> | xauth nmerge - $ xeyes
this solution keeps network isolation and is not complicated;
abstract socket, host
network driver
-
on host:
$ xauth nextract - "$DISPLAY" | <copy> $ docker run -it --net=host -e "DISPLAY" ubuntu bash
-
on guest:
$ apt-get -y update $ apt-get -y install xauth x11-apps $ touch ~/.Xauthority $ <paste> | xauth nmerge - $ xeyes
this solution is the simplest, but breaks network isolation;
abstract socket, bridge
network driver
-
on host:
$ ip addr show dev docker0 $ socat TCP-LISTEN:<port>,reuseaddr,fork,bind=<host> ABSTRACT-CONNECT:/tmp/.X11-unix/X0 $ xauth nextract - "$DISPLAY" | <copy> $ docker run -it -e "DISPLAY" ubuntu bash
-
on guest:
$ apt-get -y update $ apt-get -y install x11-apps socat $ socat ABSTRACT-LISTEN:/tmp/.X11-unix/X0,fork TCP-CONNECT:<host>:<port> $ touch ~/.Xauthority $ <paste> | xauth nmerge - $ xeyes
this solution can come into handy when xhost
on host has:
SI:localuser:<user>
if so, the steps can be simplified:
-
on host:
$ ip addr show dev docker0 $ socat TCP-LISTEN:<port>,reuseaddr,fork,bind=<host> ABSTRACT-CONNECT:/tmp/.X11-unix/X0 $ docker run -it -e "DISPLAY" ubuntu bash
-
on guest:
$ apt-get -y update $ apt-get -y install x11-apps socat $ socat ABSTRACT-LISTEN:/tmp/.X11-unix/X0,fork TCP-CONNECT:<host>:<port> $ xeyes
this saves us the effort of setting xauth
, because the connection to x server
is actually made by socat
on host; as long as this socat
is running as same
user, no xauth
work on guest needs to be done; however, beware that the relay
is unsecured, which means anyone having access to its listening port would have
access to x server, too; consider using firewalls to secure the listening port;