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.
Prerequisites
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
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!
REPOSITORY TAG IMAGE ID    CREATED    SIZE alpine     3.3 d1a6a7bfda63 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.
Alpine
- 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-mysql-cli:0.1 .
- Check the size of the image. It’s just around 38 MB
# docker images REPOSITORY            TAG IMAGE ID     CREATED       SIZE alpine-mysql-cli 0.1 83ef90a1cc0c      2 minutes ago 37.3 MB
Ubuntu
- Create a new Dockerfile for Ubuntu.
FROM ubuntu:14.04 RUN apt update    && apt install -y mysql-client    && rm -rf /var/lib/apt/lists/* ENTRYPOINT ["mysql"]
- Build Docker image from the above Dockerfile
docker build -t ubuntu-mysql-cli:0.1 .
Check the size of the image. It’s 232 MB
# docker images REPOSITORY       TAG IMAGE ID         CREATED            SIZE ubuntu-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
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 update -y RUN apt upgrade -y # Setup SSH server RUN apt 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-ssh:0.1 . Sending build context to Docker daemon 4.608 kB Sending build context to Docker daemon Step 0 : FROM ubuntu:14.04  ---> 736f02dfa6f4 Step 1 : RUN apt update -y  ---> Using cache  ---> 8ff33eb64936 Step 2 : RUN apt upgrade -y  ---> Using cache  ---> b0718fadb1cf Step 3 : RUN apt install -y openssh-server  ---> Using cache  ---> 3e97f3093be5 Step 4 : RUN mkdir /var/run/sshd  ---> Using cache  ---> f55248152d6e Step 5 : CMD /usr/sbin/sshd -D  ---> Using cache  ---> f7a298d09023 Successfully built f7a298d09023
The Best practice
An example of Docker instructions following best practices.
FROM ubuntu:14.04 # Update system and setup SSH Server RUN apt update –y && apt 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_ssh . Sending build context to Docker daemon 4.608 kB Sending build context to Docker daemon Step 0 : FROM ubuntu:14.04  ---> caf6f807d6c0 Step 1 : RUN apt-get update -y && apt-get install -y openssh-server && mkdir /var/run/sshd  ---> Using cache  ---> 6623fcc73d6b Step 2 : CMD /usr/sbin/sshd -D  ---> Using cache  ---> 1ad9952a507b Successfully built 1ad9952a507b
Step 3: Choose specific versions
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:16.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 install mysql-server
A recommended package installation example is as following.
apt install mysql-server-5.5
Step 4: Do not include sensitive data
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 MAINTAINER user@yourdomain.com # Install Apache 2.4, PHP 5.6, mysql-client-5.5 RUN apt update &&  apt upgrade -y &&  apt 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 entrypoint.sh /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 entrypoint.sh below to determine how we can use environment variables to replace DrupalMySQL DB credentials in runtime.
#!/bin/sh set -e # Apache gets grumpy about PID files pre-existing rm -f /var/run/apache2.pid # Define Drupal home file path DRUPAL_HOME="/var/www/html" # Define Drupal settings file path DRUPAL_SETTINGS_FILE="${DRUPAL_HOME}/sites/default/settings.php" # 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 fi # 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_USER='user' -e DRUPAL_MYSQL_PASS='password' -e DRUPAL_MYSQL_HOST='host' -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
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"]