Docker Best Practices: Create Production Docker Images

0
1611
Create Production Docker Images

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"]

NO COMMENTS