Ray Tsang's tips & tricks for Docker permalink
Ray Tsang, Developer Advocate at Google, gave a presentation titled Docker Tips and Tricks for Java Developers to the Chicago Java User’s Group today. Despite having run Docker in production for nearly 2 years, I got a ton out of it. Ray Tsang is also an avid traveler and photographer and can be reached on Twitter, LinkedIn and GitHub (but mostly via Twitter).
*UPDATE:* The gracious folks from Spantree recorded the session on behalf of the CJUG and you can find the video here.
Tips for all Docker containers
Minimize layers as much as possible
Each layer takes up extra space.
Combine RUN
commands into a single line if possible using the and (&&
) and pipe (|
) shell operators.
There are some caveats to this detailed in other tips below.
Incorrect
FROM debian:jessie
RUN apt-get -y update
RUN apt-get -y install curl openjdk8
Correct
FROM debian:jessie
RUN apt-get -y update && apt-get -y install curl openjdk8
Never write to the container filesystem
The filesystem sticks around long after the container has stopped and you can fill up the disk of the host machine. Instead, write your logs to stdout and if you need to write application data to disk, mount a volume in the container and write to that.
If you do happen to fill up your disk, use the following command to remove abandoned docker filesystems still on your host:
docker system prune
And when you’re running a docker container, use the --rm
flag to automatically clean up the container and its filesystem after it stops.
docker run --rm nginx
It’s handy to set up an alias
in your .bashrc
file to do this for you:
alias drrm=docker run --rm
Never use latest
When extending Docker images in your own custom Dockerfile using the FROM
directive, if you use the latest tag of that base image, it makes your Docker container build non-deterministic.
Instead, explicitly pin the version of the base image you’re extending to prevent this from happening.
Stacksmith by Bitnami can help you generate version-safe Dockerfiles for all software stacks.
On the other side, always tag your images, preferably with a semantically versioned number AND a short hash of the build
Incorrect
docker build -t uptake/myapp
Correct
docker build -t uptake/myapp:2.0-4fce3 .
Never run processes as the root user
While Docker is great at resource isolation, you don’t do everything as root for a reason.
Set a user to run the process as using the USER
directive in your Dockerfile.
Don’t chown a directory
When you COPY
files and directories into a Docker image, the operation is performed as the root user.
If you need access to those files as a different user (because you set the USER
directive as described above), you might have some trouble.
The easy solution is to include a RUN
directive that changes the permissions or ownership of those files:
FROM nginx:1.13.2-alpine
COPY . /usr/share/nginx/html
RUN chown -R nginx:nginx /usr/share/nginx/html
But in doing this, Docker creates an extra layer that’s the size of all of the files in that directory which can result in a container that’s double the size of the files within it!!!
Run a shared or remote Docker daemon
This caches base image layers, custom image layers and other dependencies.
This requires configuring the docker command to target the remote daemon.
Docker Machine offers a -d/--driver
flag when creating a new docker machine that will automatically set up the Docker host in the cloud provider of your choice:
docker-machine create -d google
Use multistage builds in single Dockerfile to build your application and export results into smaller base image
Doing so caches your build dependencies in the remote docker host, shortening build times further. The resulting image is smaller and lighter (especially if you use an alpine linux base image).
FROM maven as BUILD
COPY . /src
RUN mvn -f /src/pom.xml package
FROM openjdk:8u131-alpine
COPY --from=BUILD lib /opt/lib
COPY --from=BUILD app.jar /opt/app.jar
ENTRYPOINT ["java", "-jar", "/opt/app.jar"]
Compress your Docker images
Use the --compress
flag to compress the build context payload sent to the Docker daemon to further speed up builds:
docker build --compress -t uptake/myapp:2.0-4fce3 .
Docker containers are just processes
You can pipe data to them via stdin
and read data out of them using stdout
.
cat setup.json | docker run -i ubuntu /bin/bash -c 'jq .' | cat
Use an orchestrator
Container orchestration frameworks come with a lot of handy features that make running containers at scale easy-peasy-nice-and-easy. We use Apache Mesos (via Mesosphere DC/OS) here at Uptake. Other popular orchestrators are Kubernetes and Docker Swarm.
Tips for Java Docker containers
Copy your dependencies in as a separate layer
Doing so caches your dependencies (which rarely changes) resulting in faster build times. Do this directly before copying your application code, which changes often and should be the last layer in your Docker image.
This also means you can…
Build your application jar without your dependencies
This creates a smaller binary to COPY into the Docker image which results in a smaller layer, which results in a smaller overall Docker container.
JVM flags
If you run your JVM app in a container, you MUST set:
Max Heap
Failing to set this when running your application will result in the application claiming all of the available memory as heap space. If you app proceeds to then use that available heap space and you’ve allocated less than than when running the container, Docker may kill your container. This could result in your orchestrator unnecessarily shuffling Docker containers across hosts in the cluster!
In your Dockerfile, when defining the ENTRYPOINT
directive that executes your Java application, make sure to set the -Xmx
flag:
FROM maven as BUILD
COPY . /src
RUN mvn -f /src/pom.xml package
FROM openjdk:8u131-alpine
COPY --from=BUILD lib /opt/lib
COPY --from=BUILD app.jar /opt/app.jar
ENTRYPOINT ["java", "-Xmx200m", "-jar", "/opt/app.jar"]
ParallelGCThreads
Failing to set this will make the JVM think it can use all of the available virtual CPU cores for garbage collection.
This makes sense if the JVM uses the system and its resources exclusively.
But when there are multiple JVM processes on the same system, you could negatively impact the performance of other containers running on your host by claiming more cores for more threads than you need.
This feels like a weird leaky abstraction of the underlying resources by a tool that prides itself on resource isolation.
To get around this, use the -XX:ParallelGCThreads=<value>
flag to prevent your container from being a bad neighbor.
FROM maven as BUILD
COPY . /src
RUN mvn -f /src/pom.xml package
FROM openjdk:8u131-alpine
COPY --from=BUILD lib /opt/lib
COPY --from=BUILD app.jar /opt/app.jar
ENTRYPOINT ["java", "-XX:ParallelGCThreads=2", "-Xmx200m", "-jar", "/opt/app.jar"]
UseCGroupMemoryLimitForHeap
The JDK 8u131 has a backported feature from JDK 9 that enables the JVM to detect memory availability when running in a Docker container.
FROM maven as BUILD
COPY . /src
RUN mvn -f /src/pom.xml package
FROM openjdk:8u131-alpine
COPY --from=BUILD lib /opt/lib
COPY --from=BUILD app.jar /opt/app.jar
ENTRYPOINT ["java", "-XX:ParallelGCThreads=2", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap", "-Xmx200m", "-jar", "/opt/app.jar"]
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
Please note that this is an experimental flag that requires enabling!
Carlos Sanches goes into deeper detail in a dzone blog post.