In this article we’ll focus on how to dockerize a Spring Boot Application to run it in an isolated environment, a.k.a. container.
Furthermore we’ll show how to create a composition of containers, which depend on each other and are linked against each other in a virtual private network. We’ll also see how they can be managed together with single commands.
Let’s start by creating a Java-enabled, lightweight base image, running Alpine Linux.
2. Common Base Image
We’re going to be using Docker’s own build-file format: a Dockerfile.
A Dockerfile is in principle, a linewise batch file, containing commands to build an image. It’s not absolutely necessary to put these commands into a file, because we’re able to pass them to the command-line, as well – a file is simply more convenient.
So, let’s write our first Dockerfile:
RUN apk add --no-cache openjdk8
COPY files/UnlimitedJCEPolicyJDK8/* \
FROM: The keyword FROM, tells Docker to use a given image with its tag as build-base. If this image is not in the local library, an online-search on DockerHub, or on any other configured remote-registry, is performed
MAINTAINER: A MAINTAINER is usually an email address, identifying the author of an image
RUN: With the RUN command, we’re executing a shell command-line within the target system. Here we utilizing Alpine Linux’s package manager apk to install the Java 8 OpenJDK
COPY: The last command tells Docker to COPY a few files from the local file-system, specifically a subfolder to the build directory, into the image in a given path
REQUIREMENTS: In order to run the tutorial successfully, you have to download the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files from Oracle. Simply extract the downloaded archive into a local folder named ‘files’.
To finally build the image and store it in the local library, we have to run:
docker build --tag=alpine-java:base --rm=true.
NOTICE: The –tag option will give the image its name and –rm=true will remove intermediate images after it has been built successfully. The last character in this shell command is a dot, acting as a build-directory argument.
FROM: As base for our image we will take the Java-enabled Alpine Linux, created in the previous section
COPY: We let Docker copy our jar file into the image
ENV: This command lets us define some environment variables, which will be respected by the application running in the container. Here we define a customized Spring Boot Application configuration, to hand-over to the jar-executable later
ENTRYPOINT/CMD: This will be the executable to start when the container is booting. We must define them as JSON-Array, because we will use an ENTRYPOINT in combination with a CMD for some application arguments
VOLUME: Because our container will be running in an isolated environment, with no direct network access, we have to define a mountpoint-placeholder for our configuration repository
EXPOSE: Here we are telling Docker, on which port our application is listing. This port will be published to the host, when the container is booting
To create an image from our Dockerfile, we have to run ‘docker build’, like before:
$> docker build --file=Dockerfile.server \
But before we’re going to run a container from our image, we have to create a volume for mounting:
First, we have to –name our container. If not, one will be automatically chosen
Then, we must –publish our exposed port (see Dockerfile) to a port on our host. The value is given in the form ‘host-port:container-port’. If only a container-port is given, a randomly chosen host-port will be used. If we leave this option out, the container will be completely isolated
The –volume option gives access to either a directory on the host (when used with an absolute path) or a previously created Docker volume (when used with a volume-name). The path after the colon specifies the mountpoint within the container
As argument we have to tell Docker, which image to be used. Here we have to give the image-name from the previously ‘docker build‘ step
Some more useful options:
-it – enable interactive mode and allocate a pseudo-tty
-d – detach from the container after booting
If we ran the container in detached mode, we can inspect its details, stop it and remove it with the following commands:
$> docker inspect config-server
$> docker stop config-server
$> docker rmconfig-server
4. Dockerize Dependent Applications in a Composite
Docker commands and Dockerfiles are particularly suitable for creating individual containers. But if you want to operate on a network of isolated applications, the container management quickly becomes cluttered.
To solve that, Docker provides a tool named Docker Compose. This comes with an own build-file in YAML format and is better suited in managing multiple containers. For example: it is able to start or stop a composite of services in one command, or merges logging output of multiple services together into one pseudo-tty.
Let’s build an example of two applications running in different docker containers. They will communicate with each other and be presented as “single unit” to the host system. We will build and copy the spring-cloud-config/client example described in the spring cloud configuration tutorial to our files folder, like we have done before with the config-server.
version: Specifies which format version should be used. This is a mandatory field. Here we use the newer version, whereas the legacy format is ‘1’
services: Each object in this key defines a service, a.k.a container. This section is mandatory
build: If given, docker-compose is able to build an image from a Dockerfile
context: If given, it specifies the build-directory, where the Dockerfile is looked-up
dockerfile: If given, it sets an alternate name for a Dockerfile
image: Tells Docker which name it should give to the image when build-features are used. Otherwise it is searching for this image in the library or remote-registry
networks: This is the identifier of the named networks to use. A given name-value must be listed in the networks section
volumes: This identifies the named volumes to use and the mountpoints to mount the volumes to, separated by a colon. Likewise in networkssection, a volume-name must be defined in a separate volumes section
links: This will create an internal network link between this service and the listed service. This service will be able to connect to the listed service, whereby the part before the colon specifies a service-namefrom the services section and the part after the colon specifies the hostname at which the service is listening on an exposed port
depends_on: This tells Docker to start a service only, if the listed services have started successfully. NOTICE: This works only at container level! For a workaround to start the dependent applicationfirst, see config-client-entrypoint.sh
logging: Here we are using the ‘json-file’ driver, which is the default one. Alternatively ‘syslog’ with a given address option or ‘none’ can be used
networks: In this section we’re specifying the networks available to our services. In this example we let docker-compose create a named network of type ‘bridge’ for us. If the option external is set to true, it will use an existing one with the given name
volumes: This is very similar to the networks section
Before we continue, we will check our build-file for syntax-errors:
$> docker-compose config
This will be our Dockerfile.client to build the config-client image from. It differs from the Dockerfile.server in that we additionally install OpenBSD netcat (which is needed in the next step) and make the entrypoint executable:
RUN chmod 755 /opt/spring-cloud/bin/config-client-entrypoint.sh
And this will be the customized entrypoint for our config-client service. Here we use netcat in a loop to check whether our config-server is ready. You have to notice, that we can reach our config-server by its link-name, instead of an IP address:
while! nc -z config-server 8888 ; do
echo"Waiting for upcoming Config Server"
java -jar /opt/spring-cloud/lib/config-client.jar
Finally we can build our images, create the defined containers and start it in one command:
$> docker-compose up --build
To stop the containers, remove it from Docker and remove the connected networks and volumes from it, we can use the opposite command:
$> docker-compose down
A nice feature of docker-compose is the ability to scale services. For example, we can tell Docker to run one container for the config-server and three containers for the config-client.
But for this to work properly, we have to remove the container_name from our docker-compose.yml, for letting Docker choose one, and we have to change the exposed port configuration, to avoiding clashes.
After that, we are able to scale our services like so: