Create Advanced Docker Images in 5 Steps

Docker Moby

I originally published this article for Medium back in 2018. I moved completely from Medium to this blog. 🙂

Before dive in to the docker images, let’s have some idea about Docker first.

What is Docker?

Docker is a tool of delivering software applications as separate packages using containers. It will be much easier to create and run applications using Docker containers as a single package.

How does Docker work?

Docker use Linux kernel features such as cgroups and namespaces to create containers on the underlying operating system. Docker containers act as separate virtual machines which contain their own software libraries and configuration options. It’s more light-weight than traditional containers.

What is a Docker image?

Docker image is a standalone package that use to run an application in a completely isolated environment as a Docker container. It has all the required files and tools to run an application.

What is a Dockerfile?

Dockerfile is the base of a Docker image. It is just a simple text file contains all the required instructions and commands to create an image.

What is Docker Hub?

Docker Hub is the public repository to share Docker container images. You can push and pull docker images using Docker Hub by creating a Docker Hub account.

Create a Docker Image

Since we know the basics of Docker, we can go through the following docker tutorial to create Docker images in 5 easy steps.

Use light weight Base Docker Images

Light 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 lightweight base image for your custom 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 can install mysql-client package in both Alpine and Ubuntu base images and identify the size difference.


  • Create a new Dockerfile with Alpine.
FROM alpine:3.3RUN apk add --no-cache mysql-clientENTRYPOINT ["mysql"]
  • Build 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 with Ubuntu.
FROM ubuntu:14.04
RUN apt-get update 
    && apt-get install -y mysql-client 
    && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["mysql"]
  • Build 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
REPOSITORY             TAG  IMAGE ID      CREATED             SIZE
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.

Reduce intermediate layers

Docker 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

Choose specific versions

Docker Images with 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

Do not include sensitive data

Docker 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 Docker images

To overcome this limitation, we can use environment variables effectively. We’ll consider a production scenario regarding a Drupal and MySQL 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
MAINTAINER [email protected]

# 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.

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 light weight, reliable and secure Docker images by following this comprehensive guide.

Happy Dockerize your apps folks.

Leave a Reply

Your email address will not be published. Required fields are marked *