Monday, August 6, 2018

Running Spring Tool Suite and other GUI applications from Docker containers

Running an application within a Docker container helps in isolating the application from the host OS. Running GUI applications like for example an IDE from a Docker container, can be challenging. I'll explain several of the issues you might encounter and how to solve them. For this I will use Spring Tool Suite as an example. The code (Dockerfile and docker-compose.yml) can also be found here. Due to (several) security concerns, this is not recommended in a production environment.



Running a GUI from a Docker container

In order to run a GUI application from a Docker container and display its GUI on the host OS, several steps are needed;

Which display to use?

The container needs to be aware of the display to use. In order to make the display available, you can pass the DISPLAY environment variable to the container. docker-compose describes the environment/volume mappings/port mappings and other things of docker containers. This makes it easier to run containers in a quick and reproducible way and avoids long command lines.

docker-compose

You can do this by providing it in a docker-compose.yml file. See for example below. The environment indicates the host DISPLAY variable is passed as DISPLAY variable to the container.


Docker

In a Docker command (when not using docker-compose), you would do this with the -e flag or with --env. For example;

docker run --env DISPLAY=$DISPLAY containername

Allow access to the display

The Docker container needs to be allowed to present its screen on the Docker host. This can be done by executing the following command:

xhost local:root

After execution, during the session, root is allowed to use the current users display. Since the Docker daemon runs as root, Docker containers (in general!) now can use the current users display. If you want to persist this, you should add it to a start-up script.

Sharing the X socket

The last thing to do is sharing the X socket (don't ask me details but this is required...). This can be done by defining a volume mapping in your Docker command line or docker-compose.yml file. For Ubuntu this looks like you can see in the image below.


Spring Tool Suite from a Docker container

In order to give a complete working example, I'll show how to run Spring Tool Suite from a Docker container. In this example I'm using the Docker host JVM instead of installing a JVM inside the container. If you want to have the JVM also inside the container (instead of using the host JVM), look at the following and add that to the Dockerfile. As a base image I'm using an official Ubuntu image.

I've used the following Dockerfile:

FROM ubuntu:18.04

MAINTAINER Maarten Smeets <maarten.smeets@amis.nl>

ARG uid

LABEL nl.amis.smeetsm.ide.name="Spring Tool Suite" nl.amis.smeetsm.ide.version="3.9.5"

ADD https://download.springsource.com/release/STS/3.9.5.RELEASE/dist/e4.8/spring-tool-suite-3.9.5.RELEASE-e4.8.0-linux-gtk-x86_64.tar.gz /tmp/ide.tar.gz

RUN adduser --uid ${uid} --disabled-password --gecos '' develop

RUN mkdir -p /opt/ide && \
    tar zxvf /tmp/ide.tar.gz --strip-components=1 -C /opt/ide && \
    ln -s /usr/lib/jvm/java-10-oracle /opt/ide/sts-3.9.5.RELEASE/jre && \
    chown -R develop:develop /opt/ide && \
    mkdir /home/develop/ws && \
    chown develop:develop /home/develop/ws && \
    mkdir /home/develop/.m2 && \
    chown develop:develop /home/develop/.m2 && \
    rm /tmp/ide.tar.gz && \
    apt-get update && \
    apt-get install -y libxslt1.1 libswt-gtk-3-jni libswt-gtk-3-java && \
    apt-get autoremove -y && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* && \
    rm -rf /tmp/*

USER develop:develop
WORKDIR /home/develop
ENTRYPOINT /opt/ide/sts-3.9.5.RELEASE/STS -data /home/develop/ws

The specified packages are required to be able to run STS inside the container and create the GUI to display on the host.

I've used the following docker-compose.yml file:

version: '3'

services:
    sts:
        build:
            context: .
            dockerfile: Dockerfile
            args:
                uid: ${UID}
        container_name: "sts"
        volumes:
            - /tmp/.X11-unix:/tmp/.X11-unix
            - /home/develop/ws:/home/develop/ws
            - /home/develop/.m2:/home/develop/.m2
            - /usr/lib/jvm/java-10-oracle:/usr/lib/jvm/java-10-oracle
            - /etc/java-10-oracle:/etc/java-10-oracle
        environment:
            - DISPLAY
        user: develop
        ports:
            "8080:8080"

Notice this docker-compose file has some dependencies on the host OS. It expects a JDK 10 to be installed in /usr/lib/jvm/java-10-oracle with configuration in /etc/java-10-oracle. Also it expects to find /home/develop/ws and /home/develop/.m2 to be present on the host to be mapped to the container. The .X11-unix mapping was already mentioned as needed to allow a GUI screen to be displayed. There are also some other things which are important to notice in this file.

User id

First the way a non-privileged user is created inside the container. This user is created with a user id (uid) which is supplied as a parameter. Why did I do that? Files in mapped volumes which are created by the container user will be created with the uid which the user inside the container has. This will cause issues if inside the container the user has a different uid as outside of the container. Suppose I run the container onder a user develop. This user on the host has a uid of 1002. Inside the container there is also a user develop with a uid of 1000. Files on a mapped volume are created with uid 1000; the uid of the user in the container. On the host however, uid 1000 is a different user. These files created by the container cannot be accessed by the develop user on the host (with uid 1002). In order to avoid this, I'm creating a develop user inside the VM with the same uid as the user used outside of the VM (the user in the docker group which gave the command to start the container).

Workspace folder and Maven repository

When working with Docker containers, it is a common practice to avoid storing state inside the container. State can be various things. I consider the STS application work-space folder and the Maven repository among them. This is why I've created the folders inside the container and mapped them in the docker-compose file to the host. They will use folders with the same name (/home/develop/.m2 and /home/develop/ws) on the host.

Java

My Docker container with only Spring Tool Suite was big enough already without having a more than 300Mb JVM inside of it (on Linux Java 10 is almost double the size of Java 8). I'm using the host JVM instead. I installed the host JVM on my Ubuntu development VM as described here.

In order to use the host JVM inside the Docker container, I needed to do 2 things:

Map 2 folders to the container:


And map the JVM path to the JRE folder onder STS: ln -s /usr/lib/jvm/java-10-oracle /opt/ide/sts-3.9.5.RELEASE/jre.

Seeing it work

Allow access to the display:

xhost local:root

Next make available the variable UID to the build:

export UID=$UID

Then build:

docker-compose build

Building sts
Step 1/10 : FROM ubuntu:18.04
 ---> 735f80812f90
Step 2/10 : MAINTAINER Maarten Smeets <maarten.smeets@amis.nl>
 ---> Using cache
 ---> 69177270763e
Step 3/10 : ARG uid
 ---> Using cache
 ---> 85c9899e5210
Step 4/10 : LABEL nl.amis.smeetsm.ide.name="Spring Tool Suite" nl.amis.smeetsm.ide.version="3.9.5"
 ---> Using cache
 ---> 82f56ab07a28
Step 5/10 : ADD https://download.springsource.com/release/STS/3.9.5.RELEASE/dist/e4.8/spring-tool-suite-3.9.5.RELEASE-e4.8.0-linux-gtk-x86_64.tar.gz /tmp/ide.tar.gz


 ---> Using cache
 ---> 61ab67d82b0e
Step 6/10 : RUN adduser --uid ${uid} --disabled-password --gecos '' develop
 ---> Using cache
 ---> 679f934d3ccd
Step 7/10 : RUN mkdir -p /opt/ide &&     tar zxvf /tmp/ide.tar.gz --strip-components=1 -C /opt/ide &&     ln -s /usr/lib/jvm/java-10-oracle /opt/ide/sts-3.9.5.RELEASE/jre &&     chown -R develop:develop /opt/ide &&     mkdir /home/develop/ws &&     chown develop:develop /home/develop/ws &&     rm /tmp/ide.tar.gz &&     apt-get update &&     apt-get install -y libxslt1.1 libswt-gtk-3-jni libswt-gtk-3-java &&     apt-get autoremove -y &&     apt-get clean &&     rm -rf /var/lib/apt/lists/* &&     rm -rf /tmp/*
 ---> Using cache
 ---> 5e486a4d6dd0
Step 8/10 : USER develop:develop
 ---> Using cache
 ---> c3c2b332d932
Step 9/10 : WORKDIR /home/develop
 ---> Using cache
 ---> d8e45440ce31
Step 10/10 : ENTRYPOINT /opt/ide/sts-3.9.5.RELEASE/STS -data /home/develop/ws
 ---> Using cache
 ---> 2d95751237d7
Successfully built 2d95751237d7
Successfully tagged t_sts:latest

Next run:

docker-compose up


When you run a Spring Boot application on port 8080 inside the container, you can access it on the host on port 8080 with for example Firefox.