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"]