Recommendations for Using Docker

Juan Cabrera
May 5, 2020

Docker has changed the way we develop software. It allows us to streamline our development environments as well as our deployment story, but it can be easy to fall into traps when writing your first Dockerfiles. Here are a few things that helped mitigate some common problems we ran into when we started our Docker journey.

Do not create multiple images for the same software.

One of the big advantages of using Docker to build, run, and deploy your applications is that it allows you to have a consistent running environment for your programs. This allows you to make sure the software you run in production is the same as the software you run in your staging server, etc., which in turn simplifies your deployment process and testing capabilities.

A common mistake you might make when starting to create your Docker containers is to bundle your configuration files or runtime parameters alongside your software. This means that you will most likely need a different Docker image for each one of your environments, partially negating the benefits of having a containerized application.

Command Line Arguments

Suppose you need to pass different command line arguments to your program based on your execution environment, you can use the ENTRYPOINT directive to allow you to run your program and provide the command line arguments needed, e.g.:

# Dockerfile
FROM ubuntu:18.04
CMD ["-shl", "/"]

You can now build and run it like so:

$ docker build --tag myls .
$ docker run myls
$ docker run myls -la /bin

You can see that we are able to use the container as if it was the ls utility. Also note that we can provide default parameters by specifying them in the CMD directive.

Configuration Files and Environment Variables

Environment variables are usually the go-to solution when dealing with configuration settings, you can easily set them one by one with Docker's CLI, or by using an env-file, e.g.:

# Dockerfile
FROM ubuntu:18.04
CMD ["sh", "-c", "echo ${SOME_TEXT} ${SOME_OTHER_TEXT}"]

Build and run:

$ docker build --tag myecho .
$ docker run -e SOME_TEXT=Hello -e SOME_OTHER_TEXT=World myecho
Hello World

You can also use an env-file instead of passing each environment variable one by one:

# file.env
$ docker run --env-file file.env myecho
Hello, Bob

Sometimes your software requires a configuration file to be present somewhere inside your container, this can also be accommodated without bundling said file alongside your program, e.g.:

# Dockerfile
FROM ubuntu:18.04
CMD ["sh", "-c", "cat /config.txt"]
# config-pro.txt
Production config file :D
$ docker build --tag myconfig .
$ docker run -v /full/path/to/config-pro.txt:/config.txt myconfig
Production config file :D

You might want to use a config directory instead of a config file, e.g.:

# Dockerfile
FROM ubuntu:18.04
CMD ["sh", "-c", "cat /config/*.txt"]

Now create a folder called my-config and write two files in it.

# a.txt
This is file A
# b.txt
This is file B
$ docker build --tag mydir .
$ docker run -v /full/path/to/my-config:/config mydir
This is file A
This is file B

Local Environment

Your local environment is usually one exception to this rule. When you develop software you need a fast turnaround time from coding to running your program, this usually means that you need to install development versions of your dependencies, build your software in Debug mode and possibly run your programs in different operating modes.

You get this functionality by using multi-stage builds. You’ll be able to use one Dockerfile to build, run and test the development version of your software and build the final production image as well.

Write your logs to stdout and/or stderr.

Your application should output its logs to stdout as much as possible, this will make your life easier when it comes to inspecting your software's output and collecting logs into a database of your choosing. Whatever is outputted to stdout or stderr is automatically collected by Docker into a container specific JSON file that you can inspect by running the docker logs <container-id> command.

Refrain from using multiline logs as these are hard to group together and complicate searching your logs for key values.

Modify how your language/framework of choosing outputs its exceptions, errors and stacktraces (most have a mechanism by which to do this) so that they display all the necessary information on a single line; you might want to look into outputting them as JSON strings.

Use multi-stage builds.

Multi-stage builds allow you to use one Dockerfile to build your software and create your final Docker image, this will help reduce the size of your containers and simplify your building process. There's a great explanation on how to use this feature in Docker's documentation: Multi-Stage Builds.

By following this simple advice, you'll be able to create containers that can be easily managed and can be used in a variety of environments, from your custom deployed architecture to your highly scalable Kubernetes clusters.

"Recommendations for Using Docker" by Juan Cabrera is licensed under CC BY SA. Source code examples are licensed under MIT.

Photo by Guillaume Bolduc.

Categorized under research & learning.

We are Sophilabs

A software design and development agency that helps companies build and grow products by delivering high-quality software through agile practices and perfectionist teams.