This article will cover some really cool techniques and useful tricks which I learned during developing Docker containers.

Make Use of Docker’s Cache

Docker saves each commit (each RUN) in a separate layer. Make use of this caching mechanism while planning your application containers.

Let’s consider an PHP application example. The libraries for this application are installed through the Composer package manager. To make use of the cache we will add the following to our Dockerfile:

1 # Install libraries through composer
2 # Until the composer.json
3 ADD ./app/composer.json /usr/lib/composer/composer.json
4 
5 # Install all Wordpress dependencies
6 ENV COMPOSER_HOME '/usr/lib/composer'
7 RUN cd /usr/lib/composer && composer install

Later in our Docker start/init script (which is used while running the container) we symlink those files/libraries where they are supposed to be:

# Create symlinks to libraries downloaded through composer
ln -s /usr/lib/composer/vendor /var/www/vendor

This allows us to spin up containers really fast!

If we would run composer install for the first time in our start/init script the run process would take at least as long as the composer install, which usually takes a lot of time.

Note
This scenario implies that a container is build for a specific application.

Read more about this technique here.

Minimize Commit/Layer Number… Wisely

Remember that each commit represents a separate file system layer and that the number of commits is limited.

You can check the maximum limit of commits with the following script:

#!/bin/bash

# Remove the commit if it exists
docker rmi czerasz/aufs-layers-test

# Create a base container
docker run --name="aufs-layers-test-container" \
           ubuntu /bin/mkdir /data

# Commit the changes to a new image
docker commit --message='First commit' 'aufs-layers-test-container' czerasz/aufs-layers-test

# Remove the container
docker rm 'aufs-layers-test-container'

for i in {1..1000}
do

echo -e "\n$i build\n"

# Add another commit
cat <<-EOF |
FROM czerasz/aufs-layers-test

RUN /bin/echo -e "test\n" >> /data/test
EOF
docker build -t czerasz/aufs-layers-test -

done

On my machine it returned:

Cannot create container with more than 127 parents

Which means that there are only 127 commits possible.

This is the reason why one should combine the RUN commands in the Dockerfile.

A real word example (which I use in my base image) is presented below:

RUN apt-get install -y build-essential && \
    apt-get install -y software-properties-common && \
    apt-get install -y pwgen \
                       python-software-properties \
                       vim \
                       curl \
                       wget \
                       git \
                       unzip \
                       tree

The downside of this construction is that whenever I need to remove/add a package, Docker is not able to use it’s cache feature and all packages need to be installed from the scratch. And this takes time.

Separate Installation and Configuration

Add configuration files in the Dockerfile as late as possible. The configuration is something which changes quite often and it would revalidate the cache with each change.

Analyze this Wordpress Dockerfile as an example:

# --- PHP ---

# Install PHP
RUN apt-get install -y php5-fpm \
                       php5-mysql \
                       php-apc

# Wordpress Requirements
RUN apt-get install -y php5-curl \
                       php5-gd \
                       php5-intl \
                       php-pear \
                       php5-imagick \
                       php5-imap \
                       php5-mcrypt \
                       php5-memcache \
                       php5-ming \
                       php5-ps \
                       php5-pspell \
                       php5-recode \
                       php5-sqlite \
                       php5-tidy \
                       php5-xmlrpc \
                       php5-xsl

# --- NGINX ---

....

# ------ PHP-FPM CONFIGURATION ------
RUN mkdir -p /var/log/php5-fpm/
# Configure the php5-fpm  process and the default pool
ADD ./config/php-fpm/php-fpm.conf /etc/php5/fpm/php-fpm.conf
ADD ./config/php-fpm/pool.d/www.conf /etc/php5/fpm/pool.d/www.conf
ADD ./config/php-fpm/php.ini /etc/php5/fpm/php.ini

When we change the PHP configuration the build process will still work really fast, because the most time consuming part, the package installation is cached.

I even tend to split the installation and configuration between separate Dockerfiles.

Always Name Containers

While running a new containers rigorously use the --name= option. This way you will always know what this container was ment for.

Some real world examples are presented below: - czerasz-web - czerasz-database - czerasz-data-container

Note
A human readable name is very useful while linking containers and while working with data containers.

Use REFRESHED_AT Variable for Better Cache Control

Consider the following Dockerfile:

FROM ubuntu

# Set the reset cache variable
ENV REFRESHED_AT 2014-11-01

...

By changing the value of the REFRESHED_AT variable you are able to flush the whole cache.

Use a Supervisor for Multiple Proccesses

If Your container wraps multiple applications/processes use a process control system tool like supervisord.

A sample /etc/supervisor/conf.d/supervisord.conf which manages Serf, PHP-FPM, Nginx is presented below:

[supervisord]
pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=true                ; (start in foreground if true;default false)


[program:php5-fpm]
command=/usr/sbin/php5-fpm -c /etc/php5/fpm
autorestart=true

[program:nginx]
command=nginx
autostart=true
autorestart=true

[program:serf]
command=/etc/serf/serf-start.sh
numprocs=1
autostart=true
autorestart=true

[program:serf-join]
command=/etc/serf/serf-join.sh
autorestart=true
exitcodes=0

Use Image Inheritance

Don’t be afraid of creating Docker images. Separate your logic into different images which inherit from each other.

An example is presented below:

ubuntu:latestczerasz/baseczerasz/wordpress-baseczerasz/czerasz-wordpress-blog

Use Data Containers

A data container is a concept which is based on a simple idea to keep the data in a special container.

Then by using the option --volumes-from we attach the files from the data container to our application/database container.

The data container’s status will be Exited but don’t fear that someone will delete it by accident. Until it’s used by another container the command docker rm data-container will not work.

A real word example is a Wordpress Docker setup in which the uploads are kept in a separate data container. Let’s see how it works!

Create data container:

docker run -v /data/uploads \
           --name czerasz-data-container \
           -d debian chown -R www-data:www-data /data/uploads

Create a Wordpress container and attach to it the /data/uploads directory from the data container:

docker run \
       -v `pwd`/app:/var/www \
       --volumes-from czerasz-data-container \
       --link czerasz-database:db \
       -p 80:80 \
       --name czerasz-site \
       -d czerasz/wordpress-base \
       /usr/local/bin/init.sh production

Tip
Access data container files like this:

$ ls -al `docker inspect -f '' czerasz-data-container`
drwx------ 4 www-data www-data 4096 Nov  1 22:04 .
drwx------ 3 root     root     4096 Nov  1 20:14 ..
drwx------ 8 www-data www-data 4096 Nov  2 01:25 2014

Read more about data containers here.

Use Files for Environment Variables

If you have a lot of environment varaiables which need to be passed to the container use the --env-file option. It allows you to import variables from a file formated like this:

primary=mongo_1532
number_of_replicas=3
...

Always Remove Temporary Containers

There is always a situation when you need a container just for a small tasks. It could be checking a specific command, exporting files from a data container or creating a database dump.

Remember to use the --rm option. It will remove the container after it’s job is done and you will never have to deal with a container necropolis after calling docker ps -a.

A real world example (which was borrowed from here) is presented below:

docker run -it \
            --link some-mysql:mysql \
            --rm mysql sh -c 'exec mysql -h"$MYSQL_PORT_3306_TCP_ADDR" -P"$MYSQL_PORT_3306_TCP_PORT" -uroot -p"$MYSQL_ENV_MYSQL_ROOT_PASSWORD"'

Simplify Communication by Using the Hostname

Whenever you can, use the -h, --hostname="" option to give your container a logical hostname. This will make networking tasks much easier.

The example below shows a simple usecase. First we spin up a server container:

docker run --hostname="web-server" \
           --rm \
           --name host-communication-test-web-server \
           -it ubuntu bash

root@web-server:/# echo "Response from: "`hostname` > index.html && python3 -m http.server 8000

Then we spin up a client server which requests the web server:

docker run --hostname="web-client" \
           --rm \
           --link host-communication-test-web-server:web-server \
           -it ubuntu bash

root@web-client:/# apt-get update && apt-get install -y curl
root@web-client:/# curl -v web-server:8000/index.html
* Hostname was NOT found in DNS cache
*   Trying 172.17.1.57...
* Connected to web-server (172.17.1.57) port 8000 (#0)
> GET /index.html HTTP/1.1
> User-Agent: curl/7.35.0
> Host: web-server:8000
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: SimpleHTTP/0.6 Python/3.4.0
< Date: Tue, 25 Nov 2014 11:41:52 GMT
< Content-type: text/html
< Content-Length: 26
< Last-Modified: Tue, 25 Nov 2014 11:40:14 GMT
<
Response from: web-server
* Closing connection 0

As you can see we don’t have to use any environment variables (which we by the way don’t have if no port is exposed). We can just rely on the hostname.

What is a Docker Start/Init Script?

A Docker start/init script is simply a script which is executed when the container starts.

The script can be called manually while one runs a container:

docker run -it user_name/image_name /usr/lib/init-script-name.sh

Or through one of the Dockerfile directives: - ENTRYPOINT - CMD

Resources: