Create Production Docker Images in 5 Steps

Image source: kyohei ito via flickr


Docker images are fundamentally hold the initialization point of a production critical implementation in Docker eco system. Docker images can effectively reduce a lot of effort you put to build a Developer/Staging/Production environment from the ground up.

Docker is getting more exiting over traditional infrastructure implementations while it leverages the extended capabilities of LXC, cgroups, namespaces and Linux Kernel.

You can refer following guide to create a production ready Docker images in 5 steps.


Following guide will require basic knowledge about Dockerfiles and Docker images. Additionally, you may have basic knowledge about Docker.

Step 1: Use light weight Base Docker Images

Image source: Sponchia via pixabay

If you plan to use Docker at highly critical production systems, where you cannot afford a downtime of a few seconds, then first thing you have to choose is a light weight base image for your custom docker image.

If you run a CoreOS Kubernetes cluster to manage Docker containers in a production environment, you need to ensure that light weight docker images are presented.
If a pod terminated unexpectedly, there is a good chance that Kubernetes will spawn a new pod in a new node. In that case, new node will need to pull the image from the beginning. If the image was bulky, you will experience a delay in pod creation, which eventually leads to service downtime.

Alpine would be a good choice because it is a minimal Docker image based on Alpine Linux with a complete package index and only around 5 MB in size!

alpine 3.3 6f4ea9f58e4e 3 weeks ago 4.81 MB

Alpine vs Ubuntu

We’ll install mysql-client package in both Alpine and Ubuntu base images and identify the size difference.


  • Create a new Dockerfile for Alpine.
FROM alpine:3.3
RUN apk add --no-cache mysql-client
ENTRYPOINT ["mysql"]
  • Build Docker image from the above Dockerfile
docker build -t alpine-with-mysql-cli:0.1 .
  • You will see the following output.
$ docker build -t alpine-with-mysql-cli:0.1 .
Sending build context to Docker daemon 2.048 kB
Step 1/3 : FROM alpine:3.3
---> 6f4ea9f58e4e
Step 2/3 : RUN apk add --no-cache mysql-client
---> Running in 1dc2edd5c2a6
(1/6) Installing mariadb-common (10.1.21-r0)
(2/6) Installing ncurses-terminfo-base (6.0-r6)
(3/6) Installing ncurses-terminfo (6.0-r6)
(4/6) Installing ncurses-libs (6.0-r6)
(5/6) Installing mariadb-client (10.1.21-r0)
(6/6) Installing mysql-client (10.1.21-r0)
Executing busybox-1.24.2-r1.trigger
OK: 41 MiB in 17 packages
---> 0d6a3aa9e469
Removing intermediate container 1dc2edd5c2a6
Step 3/3 : ENTRYPOINT mysql
---> Running in 331fd566a25c
---> 098636daad26
Removing intermediate container 331fd566a25c
Successfully built 098636daad26
  • Check the size of the image. It’s just around 38 MB
$ docker images
alpine-with-mysql-cli 0.1 098636daad26 2 minutes ago 37.3 MB


  • Create a new Dockerfile for Ubuntu.
FROM ubuntu:14.04
RUN apt-get update
&& apt-get install -y mysql-client
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["mysql"]
  • Build Docker image from the above Dockerfile
docker build -t ubuntu-with-mysql-cli:0.1 .
  • You will see the following output.
$ docker build -t ubuntu-with-mysql-cli:0.1 .
Sending build context to Docker daemon 2.048 kB
Step 1/3 : FROM ubuntu:14.04
14.04: Pulling from library/ubuntu
30d541b48fc0: Pull complete
8ecd7f80d390: Pull complete
46ec9927bb81: Pull complete
2e67a4d67b44: Pull complete
7d9dd9155488: Pull complete
Digest: sha256:62a5dce5ceccd7f1cb2672a571ebee52cad1f08eec9b57fe4965fb0968a9602e
Status: Downloaded newer image for ubuntu:14.04
---> 7c09e61e9035
Step 2/3 : RUN apt-get update && apt-get install -y mysql-client && rm -rf /var/lib/apt/lists/*
---> Running in 3b9dbb2138c6
******************** output omitted ************************
Setting up mysql-common (5.5.54-0ubuntu0.14.04.1) ...
Setting up libmysqlclient18:amd64 (5.5.54-0ubuntu0.14.04.1) ...
Setting up libdbi-perl (1.630-1) ...
Setting up libdbd-mysql-perl (4.025-1ubuntu0.1) ...
Setting up libterm-readkey-perl (2.31-1) ...
Setting up mysql-client-core-5.5 (5.5.54-0ubuntu0.14.04.1) ...
Setting up mysql-client-5.5 (5.5.54-0ubuntu0.14.04.1) ...
Setting up mysql-client (5.5.54-0ubuntu0.14.04.1) ...
Processing triggers for libc-bin (2.19-0ubuntu6.9) ...
---> c807b8cbdf47
Removing intermediate container 3b9dbb2138c6
Step 3/3 : ENTRYPOINT mysql
---> Running in bfddca9a5929
---> 07925b1a1a2d
Removing intermediate container bfddca9a5929
Successfully built 07925b1a1a2d
  • Check the size of the image. It’s 232 MB
$ docker images
ubuntu-with-mysql-cli 0.1 07925b1a1a2d About a minute ago 232 MB

Based on the results above, you can clearly see the size difference is significant between these two images. You would be the judge to select your base image according to the above results.

Step 2: Reduce intermediate layers

Image source: takeshiiiit via pixabay

A Docker image is a series of layers which combines using Union File System as a single image. This layered approach is one of the reasons Docker is so lightweight. When you change a Docker image, such as when you update an application to a new version, a new layer is built and replaces only the layer it updates. The other layers remain unchanged. To distribute the update, you only need to transfer the updated layer. Docker determines which layers need to be updated at runtime.

Keep in mind that each Docker instruction creates a new layer within the image.

Some examples of Dockerfile instructions are:

  • FROM Specify the base image
  • LABEL Specify image metadata
  • RUN Run a command
  • ADD Add a file or directory
  • ENV Create an environment variable
  • CMD Specify the process to run when launching a container from this image

It’s a best practice to reduce the usage of same instructions multiple times, which will eventually reduce the number of intermediate layers. As a result, it will automatically reduce the creation of intermediate containers as well. This approach will create a slightly smaller image than a multi layered image.

You can identify the layers of an existing image by exploring the image history.

docker history <image id>

The Bad way

An example for bad usage of Docker instructions.

FROM ubuntu:14.04

# Update system
RUN apt-get update -y
RUN apt-get upgrade -y

# Setup SSH server
RUN apt-get install -y openssh-server
RUN mkdir /var/run/sshd

# Start SSH server
CMD /usr/sbin/sshd -D

Have a closer look on the below output of above example Dockerfile. You would see four intermediate containers regarding multiple usage of RUN command.

$ docker build -t ubuntu-with-ssh:0.1 .
Sending build context to Docker daemon 2.048 kB
Step 1/6 : FROM ubuntu:14.04
---> 7c09e61e9035
Step 2/6 : RUN apt-get update -y
---> Running in 2c4138bdd4ad
*********************** output omitted ***********************
Fetched 22.5 MB in 4s (5162 kB/s)
Reading package lists...
---> 9efd185bae86
Removing intermediate container 2c4138bdd4ad
Step 3/6 : RUN apt-get upgrade -y
---> Running in 1760cc07a025
*********************** output omitted ***********************
---> dc898c96c0f6
Removing intermediate container 1760cc07a025
Step 4/6 : RUN apt-get install -y openssh-server
---> Running in 0bc09187a7a4
*********************** output omitted ***********************
---> e9ca1ceb45f3
Removing intermediate container 0bc09187a7a4
Step 5/6 : RUN mkdir /var/run/sshd
---> Running in da7564959d5a
---> 0c43034f431d
Removing intermediate container da7564959d5a
Step 6/6 : CMD /usr/sbin/sshd -D
---> Running in c8933eb4b43f
---> 2b86f4b15c01
Removing intermediate container c8933eb4b43f
Successfully built 2b86f4b15c01

The Best practice

An example of Docker instructions following best practices.

FROM ubuntu:14.04

# Update system and setup SSH Server
RUN apt-get update -y
&& apt-get upgrade -y
&& apt-get install -y openssh-server
&& mkdir /var/run/sshd

# Start SSH server
CMD /usr/sbin/sshd -D

Refer the below output regarding the above example Dockerfile. You would see only one intermediate container spawned for the combined usage of RUN commands.

$ docker build -t ubuntu-with-ssh:0.2 .
Sending build context to Docker daemon 2.048 kB
Step 1/3 : FROM ubuntu:14.04
---> 7c09e61e9035
Step 2/3 : RUN apt-get update -y && apt-get upgrade -y && apt-get install -y openssh-server && mkdir /var/run/sshd
---> Running in 4dabdcb9253e
*********************** output omitted ***********************
Processing triggers for ureadahead (0.100.0-16) ...
---> 45400ccd838c
Removing intermediate container 4dabdcb9253e
Step 3/3 : CMD /usr/sbin/sshd -D
---> Running in 9604f452923b
---> 5423c5d3dd5f
Removing intermediate container 9604f452923b
Successfully built 5423c5d3dd5f

Step 3: Choose specific versions

Image source: kyohei ito via flickr

Since image creation demands the availability of various online resources such as the base image, packages etc; it’s a good practice to choose specific versions in Docker instructions. It will keep things nice and steady for a production implementation.

Imagine if we use Ubuntu latest as the base image. It will use the currently available latest Ubuntu image for our custom Docker image. Additionally, we will setup all the software components based on the same Ubuntu version.

FROM ubuntu

When Ubuntu update the latest tag with a newer base image in Docker Hub, then you might experience some package dependency issues or incompatibilities in your production Docker image.

If you want to build an image from Ubuntu, it’s recommended to use a specific Ubuntu version rather than using the latest version.

FROM ubuntu:14.04
Always choose specific package versions to install within custom image

Avoid using generic package installation instructions, which is not recommended like following example.

apt-get install mysql-server

A recommended package installation example is as following.

apt-get install mysql-server-5.5

Step 4: Do not include sensitive data

Image source: Nikin via pixabay

Using sensitive data such as Database credentials and API keys would be a challenging task in Docker.

Do not hard code any type of login credentials within a Docker image

To overcome this limitation, we can use environment variables effectively. We’ll consider a production scenario regarding a Drupal custom Docker image.

  • Create a Dockerfile for Drupal image as follows. We use Entrypoint to run Apache in foreground while setting up Drupal MySQL DB credentials from the same ENTRYPOINT executable.
FROM debian:jessie

# Install Apache 2.4, PHP 5.6, mysql-client-5.5
RUN apt-get update &&
apt-get upgrade -y &&
apt-get install vim apache2 mysql-client-5.5
php5 php5-cli php5-common php5-curl php5-gd php5-imagick
php5-json php5-ldap php5-mcrypt php5-memcache php5-memcached
php5-mysql php5-readline php5-xmlrpc libapache2-mod-php5 php-pear -y &&
a2enmod rewrite proxy proxy_http ssl

# Copy custom files
COPY /usr/local/bin/entrypoint
ADD drupal.tar.gz /var/www/html/

# Exposing Apache
EXPOSE 80 443

# Start entrypoint
ENTRYPOINT ["entrypoint"]
  • Let’s have a look about Drupal MySQL DB settings below. You should leave the credentials blank to be replaced by the environment variables.
$databases = array (
'default' =>
array (
'default' =>
array (
'database' => '',
'username' => '',
'password' => '',
'host' => '',
'port' => '',
'driver' => 'mysql',
'prefix' => '',
  • Now refer the below to determine how we can use environment variables to replace DrupalMySQL DB credentials in runtime.
set -e

# Apache gets grumpy about PID files pre-existing
rm -f /var/run/

# Define Drupal home file path

# Define Drupal settings file path

# Check the avilability of environment variables
if [ -n "$DRUPAL_MYSQL_DB" ] && [ -n "$DRUPAL_MYSQL_USER" ] && [ -n "$DRUPAL_MYSQL_PASS" ] && [ -n "$DRUPAL_MYSQL_HOST" ] ; then
echo "Setting up Mysql DB in $DRUPAL_SETTINGS_FILE"
# Set Database
sed -i "s/'database' *=> *''/'database' => '"$DRUPAL_MYSQL_DB"'/g" $DRUPAL_SETTINGS_FILE
# Set Mysql username
sed -i "s/'username' *=> *''/'username' => '"$DRUPAL_MYSQL_USER"'/g" $DRUPAL_SETTINGS_FILE
# Set Mysql password
sed -i "s/'password' *=> *''/'password' => '"$DRUPAL_MYSQL_PASS"'/g" $DRUPAL_SETTINGS_FILE
# Set Mysql host
sed -i "s/'host' *=> *''/'host' => '"$DRUPAL_MYSQL_HOST"'/g" $DRUPAL_SETTINGS_FILE

# Start Apache in foreground
tail -F /var/log/apache2/* &
exec /usr/sbin/apache2ctl -D FOREGROUND
  • Finally, you can simply define the environment variables during the Docker runtime as follows.
docker run -d -t -i
-e DRUPAL_MYSQL_DB='database'
-e DRUPAL_MYSQL_PASS='password'
-p 80:80
-p 443:443
--name <container name>
<custom image>

Now we have a custom Docker image without any sensitive data included, which can be shared publicly without any security concerns.

Step 5: Run CMD/Entypoint from a non-privileged user

Image source: Tumisu via pixabay

It’s always a best practice to run production systems using a non-privileged user, which is better from security perspectives as well.

You might have to set proper file ownership to run some programs from a non-privileged user.

You can simply put USER entry before CMD or ENTRYPOINT in Dockerfile as follows.

# Set running user of ENTRYPOINT
USER www-data

# Start entrypoint
ENTRYPOINT ["entrypoint"]


Now you are an expert of creating production ready Docker images in 5 easy steps. You will be able to create a light weight, reliable and secure Docker image by following this comprehensive guide.

Happy Dockerize your custom apps folks.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.