Note
|
This repository contains the guide documentation source. To view the guide in published form, view it on the Open Liberty website. |
Learn how to containerize a microservice with Docker for iterative development
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.
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
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
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.
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.
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:
link:finish/Dockerfile[role=include]
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.
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
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.
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:
Flag | Description |
---|---|
-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
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.
You containerized a simple REST application. You can now continuously deliver changes to the application, and they will reflect automatically when the application rebuilds.