Cut bake and pack - Build machine images with Docker apps using Hashicorp Packer

Daniel Romić on 07 Oct 2018

Source: Wes Hicks @ unsplash.com


Just like confectioners have fun when baking a batch of French desserts, we in the IT world have fun “baking” our own machine images. Packer is a great vendor-agnostic tool for doing just this. Basic Packer details can be found at their official docs, such as the structure of the JSON file, variables, builders, provisioners etc.

What I’ll be focusing on in this blog post is working with those files and iterating our base image to have a working VirtualBox appliance with Docker installed on there, as well as basic Nginx running in Docker. I will use official Ubuntu 18.04. image and work my way up from there. This is where Packer really shines. Amongst other output formats it can export an image to OVA (OVF) format which basically enables people to build agnostic images for pretty much any platform that supports OVA (OVF) format.

Project Repo

I’ve used Jeff Geerling’s Github repository as a base to work up from.

The structure of this project is the following:

├── README.md
├── ansible
│   └── main.yml
├── bloggy-base.json
├── bloggy-docker.json
├── http
│   └── preseed.cfg
├── packer_cache
└── scripts
    └── setup.sh

packer cache folder will contain downloaded .iso image. preseed.cfg in http server for jump-starting Ubuntu installation and it’s referenced in Packer JSON configuration file as well. The setup.sh script installs Ansible and modifies user permissions for that machine.

Base Image

Let’s use virtualbox-iso for our builder. Easiest and fastest way for this is to find a ready .iso image on the web and copy and paste its link in the corresponding section in your JSON Packer file. The section mentioning this .iso source looks like this:

"iso_urls": [
  "iso/<image_name_goes_here>", # This one searches for ISO file in cache folder,
  "http://<link_goes_here>"     # This one fetches for ISO file from a remote location when no ISO file is cached.

We mentioned that we’ll use Ubuntu 18.04 ISO image:

"iso_urls": [
"iso_checksum_type": "sha256",
"iso_checksum": "a5b0ea5918f850124f3d72ef4b85bda82f0fcd02ec721be19c1a6952791c8ee8",

We’ll also need to find a checksum for this image (for Ubuntu can be found here) The idea behind a base image is to have a fresh and clean Ubuntu server installed with one defined user - say bloggard. This user is defined in the preseed.cfg file, which I’ve tampered with.

This process will take some time and after it’s finished we’ll have a pre-baked image (you may have heard this term before) image that we can then use in our next packer-build iterations.

The base image file is called bloggy-base.json

I also wish to install Ansible on this base image in order to do custom provisioning later on. This is defined in the setup.sh bash script.

Validate Packer JSON

Before running packer build <packerfile.json> it’s useful to check the config file for any syntax and some logic errors:

$ packer validate bloggy-base.json
Template validated successfully.

Build image

If you’ve received this response, you may proceed with the build and watch the output on your terminal. Optionally, you can start VirtualBox and attach to the machine being built and watch the progress.

You build an image with the following command:

$ packer build bloggy-base.json
virtualbox-iso output will be in this color.

==> virtualbox-iso: Downloading or copying Guest additions
    virtualbox-iso: Downloading or copying: file:///Applications/VirtualBox.app/Contents/MacOS/VBoxGuestAdditions.iso
==> virtualbox-iso: Downloading or copying ISO
Base image progress as shown in VirtualBox
Base image building progress as shown in VirtualBox

It will show its output in the command line, but you can track this process by opening up your VirtualBox and selecting this machine. You should be able to recognize this machine by the name we’ve given it in bloggy-base.json file. You can set the output location on your own; I chose a new folder, ./vms. The folder must not exist prior to running Packer for this case.

When Packer is done and image is ready, import it in VirtualBox and start it up. You should have a bare Ubuntu (with Ansible) installed.

Base image imported in VirtualBox
Base image imported in VirtualBox

What if build is intolerably slow?

We’ve defined virtual machine hardware resources that we assign from the host machine. To speed up the baking process, you may try to allocate more CPU cores and RAM to the virtual machine.

"vboxmanage": [

Final image?

By the time we get here we have a working basic, very boring Ubuntu image.


It seems reasonable to write bash scripts for initial image setup, provisioning, clean-up and that’s perfectly fine. There are many approaches to take that lead to the same result; however, I would advise any serious provisioning to be done through a configuration management tool, such as Ansible. In this example, we’ll install Docker via Ansible.

Ansible is already installed on our pre-baked image, and to make things very easy, we will install latest Docker with convenience script:

'bash -c "curl -fsSL https://get.docker.com/ | sh"'

Before that, it’s good to have a few of the useful programs installed as well (wget, curl, vim and python-pip for docker-py as well). Finally, let’s choose to have a web server running in Docker when we log into our new machine. The complete playbook looks like this:

- hosts: all
  become: yes
  gather_facts: yes
    - name: 'Install basic packages'
       name: "{{ item }}"
       state: "installed"
        - wget
        - python-pip
        - curl
        - vim

    - name: 'Pip install docker-py'
        name: docker-py
        state: present

    # Install Docker via convenience script - latest version
    - name: 'Install Docker'
      command: 'bash -c "curl -fsSL https://get.docker.com/ | sh"'

    - name: 'Pull Nginx Docker image and run it'
        name: nginx-bloggy
        image: nginx
        state: started
        restart: yes
        restart_policy: always
          - "80:80"

Very simple and clean.

Bakery revisited

We will use our pre-baked base image as a new source for the next iteration of Packer in a new file named bloggy-docker.json. The output folder will be separate from our pre-baked source image(s). This folder will be called ./image and it will not exist before build process as well.

Run packer validate bloggy-docker.json followed by packer build bloggy-docker.json. This time it will be more useful to track progress via the terminal output, rather than VirtualBox.

Provisioning machine image with Ansible
Provisioning of the new image with Ansible

Check Results

Import the box ./image/bloggy-docker.ovf in VirtualBox, start it and check for Nginx running in Docker.

$ sudo docker ps
Provisioning machine image with Ansible
Checking if Nginx is operational on the final image

It works, that’s it! The next question is: “Do we really need an image this big?”


  • The whole thing could be done in one (first) step of Packer. It would take more time, but if you haven’t got it from the first time, you will wait several times more to try baking/packing again than if you took the iterative approach.

comments powered by Disqus