Playing with Docker: tips and tricks to write effective Dockerfiles

Recently I have been playing with Docker containers, and I am sure you already know what Docker is. In this post I will describe what I have learnt while using Docker containers and preparing Dockerfiles.

What is Docker?

In a few words: Docker is a software to manage and run Linux containers in which you can deploy an application using Dockerfiles. The main concept here is divide-et-impera concept: a Docker container is just like a virtual machine, except that is very lightweight (it is only a container, so it is not virtualizing an entire machine).

How do I run a Docker container?

  1. Install Docker
  2. docker run container command (e.g. docker run -ti fedora bash)

Docker will fetch the container (from DockerHub, a central repo for Docker containers) and run the specified command.

What is a Dockerfile?

A Dockerfile is a set of instructions to prepare a Docker container on your own. It basically declare a base image (like a Linux distribution: for example Fedora, Ubuntu, etc.) and apply modifications on that container (fetch the software you want to run on container, compile it, run, etc). There is a basic set of instruction for Dockerfiles. For all Vagrant users: that’s right, a Dockerfile is just like a Vagrantfile: it states the steps to prepare a machine (except that in this case we are preparing a container).

How do I use a Dockerfile?

  1. Install Docker
  2. Download a Dockerfile
  3. (if applicable): edit any files that will be copied from host to container. This is useful for configuration files if you want to customize your Dockerized application
  4. docker build -t containername .

Docker will reproduce the steps to create the container, using the instructions found on the Dockerfile. After it has finished, you can run the Docker container as specified above.

Docker: from theory to practice

Ok, theory aside. I decided to create a Dockerfile for two applications because:

  • the application was not available from the official repos (e.g. alice)
  • the version in the official repos is outdated (e.g. bitlbee)

Basically, we will declare two Docker containers in which we fetch our software, customize to our needs and run it inside the container. Both of them will declare a service, and the container will serve as a server for the application (alice/http and bitlbee/irc).

bitlbee Dockerfile

In this case we are using my preferred base image which is Fedora, we customize it to be able to fetch and compile the source code of bitlbee and then proceed to compile it. In this Dockerfile we also ADD two configuration files from the host to the Dockerfile. Again, we launch the service as daemon user and expose the 6667/tcp port. The final size of the Docker container image is 359MB.

To use it, connect your IRC client to localhost:6667 (remember to map the correct port, see below).

bitlbee Dockerfile on GitHub.

Tips and caveats

Docker

First of all, some tips I learnt:

  • When running a container, it is always best to launch it with a name (it easier to reference the container afterwards): docker run --name bitlbee_container mbologna/docker/bitlbee
  • If you want to detach a container, supply -d option when running
  • You can inspect a running container by attaching to it: docker exec -ti bitlbee bash
  • Remember to clean up docker images and docker containers: show them with docker images and docker ps -a. Remove them with docker rmi and docker rm
  • If you are running Docker containers as a service (like in this example), you should remember to set the option --restart=always to make sure that your Docker container is started at boot and whenever it exits abnormally

Everything on the docker container makes it apart from the host machine under all points of view (network, fs, etc.). Thus:

  • When using Docker containers (in particular you are running a service inside a Docker container), you can access your container ports by mapping the ports on the containers to ports on your host using the -p option: docker run -p 16667:6667 mbologna/docker-bitlbee (container maps 16667 port on the host machine to 6667 port on the container, so it can be accessed at 16667/tcp on the host machine)
  • When a container is restarted, everything on the container is reset (speaking of file-system too). In order to write non-volatile files, you should supply -v option that declares a volume; as with ports we have seen above, you specify first the directory on host and then the corresponding directory on the container. This is useful for config files (you want to keep them, right?): docker run -v /home/mbologna/docker-bitlee/var/lib/bitlbee:/var/lib/bitlbee mbologna/docker-bitlbee

Dockerfiles

  • If you define a VOLUME in the Dockerfile:
    • if user is launching Docker container without specifying a volume, VOLUME directory will typically resides under /var/lib/docker/volumes (you can discover it using docker inspect <container>)
    • otherwise, VOLUME directory will resides on the specified directory using -v option.

    This exposes an issue of permissions on the VOLUME directory. I basically solved it by chowning twice the volume directory, otherwise either one of the two cases described above wouldn’t have the correct permissions: chown -R daemon:daemon /var/lib/bitlbee* # dup: otherwise it won't be chown'ed when using volumes
    VOLUME ["/var/lib/bitlbee"]
    chown -R daemon:daemon /var/lib/bitlbee* # dup: otherwise it won't be chown'ed when using volumes

  • When a final user pulls a container, it basically downloads your container from DockerHub. That’s why we want to minimize Docker container size. How can we do that when preparing a Dockerfile?
    • Every command you launch on a Dockerfile creates a new (intermediate) Docker container (the final result will be the application of every instruction on top of the instruction above it!) => minimize steps and group commands under RUN commands using &&. E.g.:

      RUN touch /var/run/bitlbee.pid && \
      chown daemon:daemon /var/run/bitlbee.pid && \
      chown -R daemon:daemon /usr/local/etc/* && \
      chown -R daemon:daemon /var/lib/bitlbee*

    • After you compiled/installed your software, be sure to remove it if unnecessary to clean up space: apt-get clean && \
      apt-get autoremove -y --purge make \
      rm -fr /var/lib/apt/lists/*

  • Every command you launch on the Docker container is run as root: be sure, before launching your software, to launch it with as minimal privileges as possible Principle of least privilege. For example, I launch alice and bitlbee daemons with the daemon user: USER daemon
    EXPOSE 8080
    CMD ["/home/alice/alice/bin/alice", "-a", "0.0.0.0"]

Contributing

You can pull my Docker containers on DockerHub:

You can browse my Dockerfiles on GitHub:

Future work

Two interesting concepts I came across during my research and I will investigate in the future:

  • CoreOS, a Linux distribution in which every application is launched on a separate Docker container
  • Kubernetes, an orchestration layer for Docker containers

Leave a Reply