Using Docker containers to develop microservices

duration 20 minutes

Learn how to containerize a microservice with Docker for iterative development

What you’ll learn

You will learn how to run and continuously develop a simple REST application with Open Liberty and Docker.

The REST application that you will run was written for you in advance and can be found in the start/src directory. To learn more about this application and how to build it, read Creating a RESTful web service.

To containerize your application, first build it with Maven and add it to the servers of your choice. Second, create a Docker image that contains an Open Liberty runtime. Third, run this image and mount a single server directory or the directory that contains all of your servers to the container’s file system. Finally, run one of the mounted servers inside of a container.

What is Docker?

Docker is a tool that you can use to deploy and run applications with containers. You can think of Docker like a virtual machine that runs various applications. However, unlike a typical virtual machine, you can run these applications simultaneously on a single system and independent of one another.

Learn more about Docker on the official Docker page: https://www.docker.com/what-docker

Learn how to install Docker on the official instructions page: https://docs.docker.com/engine/installation

What is a container?

A container is a lightweight, stand-alone package that contains a piece of software that is bundled together with the entire environment that it needs to run. Containers are small compared to regular images and can run on any environment where Docker is set up. Moreover, you can run multiple containers on a single machine at the same time in isolation from each other.

Learn more about containers on the official Docker page: https://www.docker.com/what-container

Why use containers?

Consider a scenario where you need to deploy your application on another environment. Your application works on your local machine, but when you try to run it on a different environment, it breaks. You do some debugging and discover that you built your application with Python 3, but this new environment has only Python 2.7 installed. Although this issue is generally easy to fix, you don’t want your application to be missing dozens of version-specific dependencies. You can create a virtual machine specifically for testing your application, but VM images generally take up a huge amount of space and are slow to run.

To solve the problem, you can containerize your application by bundling it together with the entire environment that it needs to run. You can then run this container on any machine that is running Docker regardless of how that machine’s environment is set up. You can also run multiple containers on a single machine in isolation from one another so that two containers that have different versions of Python do not interfere with each other. Containers are quick to run compared to individual VMs, and they take up only a fraction of the memory of a single image.

Getting started

The fastest way to work through this guide is to clone the Git repository and use the projects that are provided inside:

git clone https://github.com/openliberty/guide-docker.git
cd guide-docker

The start directory contains the starting project that you will build upon.

The finish directory contains the finished project, which is what you will build.

Building your application

Before you begin, build your application. To do this, navigate to the start directory and run the Maven clean and install goals:

mvn clean install

Your pom.xml file is already configured to add your REST application to the defaultServer server, but you can tweak this configuration or add your own for another server:

<!-- Install the app into the apps/ directory of defaultServer -->
<execution>
    <id>install-app</id>
    <phase>package</phase>
    <goals>
        <goal>install-apps</goal>
    </goals>
    <configuration>
        <appsDirectory>apps</appsDirectory>
        <stripVersion>true</stripVersion>
        <installAppPackages>project</installAppPackages>
    </configuration>
</execution>

The install-apps goal copies the application into the specified directory of the specified server. In this case, the goal copies the rest.war file into the apps directory of the defaultServer server.

Learn more about this goal on the official Maven Liberty plug-in repository.

Creating the Dockerfile

A Dockerfile is a collection of instructions for building a Docker image that can then be run as a container. Every Dockerfile begins with a parent or base image on top of which various commands are run. For example, you can start your image from scratch and execute commands that download and install Java, or you can start from an image that already contains a Java installation.

Create a regular start/Dockerfile file:

# Start with OL runtime.
FROM open-liberty

# Symlink servers directory for easier mounts.
RUN ln -s /opt/ol/wlp/usr/servers /servers

# Run the server script and start the defaultServer by default.
ENTRYPOINT ["/opt/ol/wlp/bin/server", "run"]
CMD ["defaultServer"]

The FROM instruction initializes a new build stage and indicates the parent image from which your image is built. If you don’t need a parent image, then use FROM scratch, which makes your image a base image. In this case, you’re using the openliberty/open-liberty:javaee7 image as your parent image, which comes with the latest Open Liberty runtime.

The RUN instruction executes various shell commands in a new layer on top of the current image. In this case, you create a symlink between the /opt/ol/wlp/usr/servers directory and the /servers directory. This way, you can mount your servers more easily because you don’t need to use long path names.

The CMD and ENTRYPOINT instructions define a default command that executes when the image runs as a container. These two instructions function the same way, except that the CMD instruction is overridden with any arguments that are passed at the end of the docker run command. In contrast, the ENTRYPOINT instruction requires the --entrypoint flag to be overridden. In this case, you use the ENTRYPOINT instruction to start an Open Liberty server and the CMD instruction to indicate which server to start. Because the CMD instruction is easily overridden, starting any server is convenient.

For a complete list of available instructions, see the Docker documentation.

Optional: Writing a .dockerignore file

When Docker runs a build, it sends all of the files and directories that are located in the same directory as the Dockerfile to its build context, making them available for use in instructions like ADD and COPY. To make image building faster, add all files and directories that aren’t necessary for building your image to a .dockerignore file. This excludes them from the build context.

A .dockerignore file is available to you in the start directory. This file includes the src directory, the pom.xml file, and some system files. Feel free to add anything else that you want to exclude

Building the image

To build your image, make sure that your Docker daemon is running and execute the Docker build command from the command line. If you execute your build from the same directory as your Dockerfile, you can use the period character (.) notation to specify the location for the build context. Otherwise, use the -f flag to point to your Dockerfile:

docker build -t ol-runtime .

Use the -t flag to give the image an optional name. In this case, ol-runtime is the name of your image.

The first build usually takes much longer to complete than subsequent builds because Docker needs to download all dependencies that your image requires, including the parent image.

If your build runs successfully, you’ll see an output similar to the following:

Sending build context to Docker daemon  3.072kB
Step 1/4 : FROM openliberty/open-liberty:javaee7
javaee7: Pulling from openliberty/open-liberty
660c48dd555d: Pull complete
4c7380416e78: Pull complete
421e436b5f80: Pull complete
e4ce6c3651b3: Pull complete
be588e74bd34: Pull complete
6da4611cbdb3: Pull complete
3cbd5728a38e: Pull complete
38f12bc85354: Pull complete
540c270bda6a: Pull complete
bbd56d34f762: Pull complete
8f181e95aa97: Pull complete
4c14e095e68e: Pull complete
bed46b62b8c3: Pull complete
Digest: sha256:d6793b7865d86ad43e5cc7c02089f7eefd2b1af3e312bc453e4779f4f24c28a6
Status: Downloaded newer image for openliberty/open-liberty:javaee7
 ---> d71303f77e8d
Step 2/4 : RUN ln -s /opt/ol/wlp/usr/servers /servers
 ---> Running in 191078d2ce16
 ---> dfef8073fe42
Removing intermediate container 191078d2ce16
Step 3/4 : ENTRYPOINT /opt/ol/wlp/bin/server run
 ---> Running in 38e48bae6dd7
 ---> 1015cbcdcf37
Removing intermediate container 38e48bae6dd7
Step 4/4 : CMD defaultServer
 ---> Running in 3fea454efdcd
 ---> ee99661593eb
Removing intermediate container 3fea454efdcd
Successfully built ee99661593eb
Successfully tagged ol-runtime:latest

Each step of the build has a unique ID, which represents the ID of an intermediate image. For example, step 2 has the ID dfef8073fe42, and step 4 has the ID ee99661593eb, which is also the ID of the final image. During the first build of your image, Docker caches every new layer as a separate image and reuses them for future builds for layers that didn’t change. For example, if you run the build again, Docker reuses the images that it cached for steps 2 - 4. However, if you make a change in your Dockerfile, Docker would need to rebuild the subsequent layer since this layer also changed.

However, you can also completely disable the caching of intermediate layers by running the build with the --no-cache=true flag:

docker build -t ol-runtime --no-cache=true .

Learn more about the image build process on the Docker documentation.

Containerize your application

Now that your image is built, execute the Docker run command from the command line to run it:

docker run -d --name rest-app -p 9080:9080 -p 9443:9443 -v <absolute path to guide>/start/target/liberty/wlp/usr/servers:/servers ol-runtime

Alternatively, you can also execute the run command to mount a single server instead of the whole servers directory:

docker run -d --name rest-app -p 9080:9080 -p 9443:9443 -v <absolute path to guide>/start/target/liberty/wlp/usr/servers/defaultServer:/servers/defaultServer ol-runtime

Let’s break down the flags:

FlagDescription

-d

This flag tells Docker to run the container in the background. Without this flag, Docker runs the container in the foreground.

--name

This flag gives the container a name.

-p

This flag maps the container ports to the host ports.

-v

This flag mounts a directory or file to the file system of the container.

You can pass in an optional server name at the end of the run command to override the defaultServer server in the CMD instruction. For example, if your servers directory also contains a server called testServer, then it can be started as shown in the following example:

docker run -d --name rest-app -p 9080:9080 -p 9443:9443 -v <absolute path to guide>/start/target/liberty/wlp/usr/servers:/servers ol-runtime testServer

Learn more about running containers on the official Docker page: https://docs.docker.com/engine/reference/run

Testing the container

Before you access your application from the browser, run the docker ps command from the command line to make sure that your container is running and didn’t crash:

$ docker ps
CONTAINER ID        IMAGE               CREATED             STATUS              NAMES
2720cea71700        ol-runtime          2 seconds ago       Up 1 second         rest-app

To view a full list of all available containers, run the docker ps -a command from the command line.

If your container is running without problems, point your browser to http://localhost:9080/LibertyProject/System/properties, where you can see a JSON file that contains the system properties of the JVM in your container.

Edit the src/main/java/io/openliberty/guides/rest/PropertiesResource.java class and change the @Path annotation to "properties-new". These edits change the endpoint of your application from /properties to /properties-new:

@Path("properties-new")
public class PropertiesResource {
  ...
}

To see these changes reflected in the container, run the mvn package command from the command line to rebuild your application and point your browser to http://localhost:9080/LibertyProject/System/properties-new. You see the same JSON file that you saw previously.

To stop your container, run the docker stop rest-app command from the command line.

If a problem occurs and your container exits prematurely, the container won’t appear in the container list that the docker ps command displays. Instead, your container appears with an Exited status when you run the docker ps -a command. Run the docker logs rest-app command to view the container logs for any potential problems and double-check that your Dockerfile is correct. When you find the cause of the issue, remove the faulty container with the docker rm rest-app command, rebuild your image, and start the container again.

Great work! You’re done!

You containerized a simple REST application. You can now continuously deliver changes to the application, and they will reflect automatically when the application rebuilds.

Contribute to this guide

Is something missing or broken? Raise an issue, or send us a pull request.