05 - January - 2017

Pack It In! - Packaging Docker Images With Packer

Post by Michael A Profile picture for user Michael A

Containerisation is an increasingly popular method of building and distributing software for universal usage.  Docker is one of the most prominent container based technologies enabling you to encapsulate an application, dependencies and any associated tools into a image ready to run.

If you are familiar with Docker you will know the traditional method of building images is by defining a Dockerfile and running docker build against it.  Whilst this works and results in an image that is (hopefully) usable, it is not particularly flexible.  Dockerfiles do not allow you to easily make use of conditional statements nor do they allow for complex variables to be defined.  For example during a build you may want to install specific versions of software.   Using a Dockerfile you would define the version to install as part of a RUN command, however this can become cumbersome if you are building multiple versions of an image.  For example images providing different PHP versions.

These limitations were something we found to be impacting how we built our containers and ended up being a huge constraint.  Fortunately a tool exists allowing you to have greater control over the build process allowing you to make use of your configuration management scripts, Packer.

Packer allows you to define the build process of the Docker image through json and configuration scripts.  These scripts can be shell scripts, Ansible scripts or one of the many other supported provisioners.  Through Packer you can have complete control over the build process using all the features offered by your chosen provisioner e.g. conditional statements and variables.

Before you can use Packer to build your Docker image, you must first install it.  Packer can run anywhere you can run Docker and is distributed as a single binary, though it can be installed through your package management system (if available) for example using brew.  In my case I install it locally on my Mac to build images for later use on Linux servers.

Once installed you can define your json file.  This file instructs the process for packer to follow when building your Docker image.  You define the base image, entry points and commands to run during the build.  In addition you can also tag and push the build image to a registry e.g. Amazon EC2 Container Registry.

The following is an example json file to build an image based on Ubuntu which uses shell scripts and Ansible to provision the configuration.

    "builders": [
            "type": "docker",
            "image": "ubuntu:16.04",
            "commit": "true",
            "pull": "true",
            "run_command": [
                "nginx -g 'daemon off;'"
    "provisioners": [
            "type": "shell",
            "script": "install-ansible.sh"
            "type": "ansible-local",
            "playbook_file": "common.yml"

Here we are instructing Packer to use Ubuntu 16.04 as the base Docker image and to set the entrypoint of the image as the Nginx command.  The provisioners are instructing packer to execute the install-ansible.sh script and then to execute Ansible aginst the playbook common.yml

As the Ubuntu image does not include Ansible already, we use the ansible-install.sh script to install it for us;


apt-get update
apt-get install software-properties-common -y
apt-add-repository ppa:ansible/ansible -y
apt-get update
apt-get install ansible -y

This is just a basic script adding Ansible from the official PPA repository and installing it.  Without this step Packer would not able to execute the next step as the ansible-playbook command would not exist on the container.  This step could be removed if an image was used that already had Ansible installed.

Packer can use any Ansible playbook as long it is configured to run against localhost.  The following playbook example installs nginx;


    - hosts: localhost


      - name: install nginx
          name: nginx
          state: latest
        when: ansible_distribution == "Ubuntu"

The above example will ensure that latest version of Nginx is installed when the image version is detected as Ubuntu.  If the image was not Ubuntu then the steps would not be executed and would skip over.

Once you have defined the json and Ansible playbooks to be used by Packer, you can validate the configuration by running (where filename is the name of the json file);

packer validate filename.json

This will validate that the json file is correct and that any files required exist, it will not validate the shell script or ansible playbook.  If any issues are identified it will report them where possible.

Finally, building the image is as simple as executing;

packer build filename.json

Packer will now proceed to download the base image specified and execute the provisioning steps you defined.  Each of the steps will be displayed including output from each of the tasks executed by Ansible.  If errors occur during the build process then they will be reported in the same readable format produced by Ansible.

After a short while, if the build completed without errors you will be left with the built Docker image.  If you had defined within the json to tag and push the image then this would also have been performed.

So there you have it, building docker images with all the features and customisation provided by Ansible without the limitation of a Dockerfile.  Packer is not limited to building Docker images, it is also capable of building images for a number of other services.  For example you can build your docker image, along with VirtualBox images and Amazon EC2 images all at the same time from the same set of configuration files. For more information on Packer and it’s features consult the website and documentation.

Profile picture for user Michael A

Michael A

Add new comment

Share this article

Our thoughts

Let's work together

Get in touch and find out how we can empower your organisation.
Back to top