Skip to content

Docker

Docker is a tool designed to make it easier to create, deploy, and run applications by using containers. Containers allow a developer to package up an application with all of the parts it needs, such as libraries and other dependencies, and deploy it as one package. Immutable Docker images are created from a defined Dockerfile.

Layers

A Docker image consists of read-only layers each of which represents a Dockerfile instruction. The layers are stacked and each one is a delta of the changes from the previous layer. Care must be taken when designing a Dockerfile to optimize both the time it takes to build the image as well as the size of the image that is created.

Layers reduce the time it takes to build the image through efficient use of image layers.

Order of instructions

Docker will cache layers from previous builds to decrease the time required to build images. However, once Docker detects that a layer needs to be rebuilt, then all layers after it must also be rebuilt. Consider the following Dockerfile.

FROM ubuntu
RUN apt-get update
RUN apt-get install -y openjdk-8-jdk
COPY . /app

Dependency layer

Downloading dependencies for an application can be an expensive operation. You can leverage Docker layer caching to avoid this in image build time by having a separate layer that resolves the dependencies for the application. In the following example, we have a layer to RUN gradle build before we add the source code to the image. This results in a layer with all our application dependencies downloaded.

COPY build.gradle gradle.properties settings.gradle
RUN gradle build

Multi-stage builds

One of the most challenging things about building images is keeping the image size down. Each instruction in the Dockerfile adds a layer to the image, and you need to remember to clean up any artifacts you don’t need before moving on to the next layer. With multi-stage builds, you use multiple FROM statements in your Dockerfile. Each FROM instruction can use a different base, and each of them begins a new stage of the build. You can selectively copy artifacts from one stage to another, leaving behind everything you don’t want in the final image.

Build from source

In order to ensure repeatability of image builds, you should put as much of the build process as possible inside the Dockerfile. For example, if you run Gradle to build a jar from your workspace and then have the Dockerfile copy in the jar file, you run the risk of other developers not having the same tools on their workspace. In order to mitigate that, install and run Gradle from within the Dockerfile:

COPY build.gradle gradle.properties settings.gradle
RUN gradle build
COPY . /app
RUN gradle clean build release

Separate build stage

The concern with running the build within your Dockerfile is the additional space consumed by the image for all the build tools. This can be addressed through the use of a separate stage for the build.

## Builder stage
FROM openjdk:7 as builder
COPY build.gradle gradle.properties settings.gradle
RUN gradle clean build release
COPY . /app

## Runner stage
FROM ubuntu as runner
COPY --from=builder /app/dist/web-app-1.0.1.BUILD.war app/webapps/web-app.war

Base Images

A base image is the image that is used to create all of your container images. A proper implementation of base images improve maintainability of your images. Your base image can be an official Docker image, such as Centos, or you can modify an official Docker image to suit your needs, or you can create your own base image from scratch. When building images, there are a few things to consider in regards to the base image from which to build from.

Use official images

Where possible, use official images from Docker Hub rather than installing tools manually. For example, the following Dockerfile is installing Java.

## Don't do this
FROM centos:centos6 
RUN yum install java-1.7.0-openjdk-devel -y

## Better
FROM openjdk 

Use specific tags

If you omit the tag on the FROM line, you will end up with the latest tag. This can result in inconsistencies in the image that is built as the latest tag can change between times when the image is built. Therefore, be sure to always include a tag to pin to a specific upstream image.

FROM openjdk:7

Processes

The final step in the Dockerfile is the run the application using a process command. The recommendation is to separate areas of concern by using one process per container.

Tip: Limit the amount of processes running within the container.

  • Simplicity - Running multiple process within a container requires additional scripting within the container to coordinate the startup of each process.
  • Reliability - When all process for a container exit, the container orchestrator can automatically restart the container. If there is more than one process, and it exits, it is up to you to manage the restart of the process within the container rather than allowing the orchestrator to restart it.
  • Scalability - If one process requires additional capacity and triggers an auto scaling event, then all process within the container are also scaled.

The recommendation is the exec form of the ENTRYPOINT instruction to run the one process:

ENTRYPOINT ["java", "-jar", "/app.jar"]