Hey,

oftentimes I have some code that ends up in a Docker image which I want to have pushed to Dockerhub. Docker itself is capable of automatically building images from git repositories but it has the restriction that I can’t specify a certain Docker daemon version to run the build. This used to be important (not sure if it still is) as in the stable version of Docker we already have been using multi-stage builds but the Dockerhub builder didn’t allow us to use it.

As I already make use of Travis to run my tests I think it’s a no-brainer to tie the image building to Travis as well - if tests pass and we’re on the master branch, build the image and push to docker hub.

Updating the docker daemon

As I mentioned, I like to force Travis to use a newer Docker than they use by default. To speed some things I also like to configure /etc/docker.json so that we can increase the concurrency of downloads and uploads (it really matters depending on how many images you have and whether you build them in parallel).

To achieve that I use the following structure:

.
├── .travis
│   └── main.sh
├── .travis.yml
├── README.md
├── Dockerfile
└── Makefile

.travis is a directory that holds scripts to update the Travis machine. This way if I need to use the debug feature of Travis I don’t have to follow the entire .travis.yml recipe and instead just run ./.travis/main.sh from there to have the dependencies set.

The file looks like the following:

#!/bin/bash

# Set an option to exit immediately if any error appears
set -o errexit

# Main function that describes the behavior of the 
# script. 
# By making it a function we can place our methods
# below and have the main execution described in a
# concise way via function invocations.
main() {
  setup_dependencies
  update_docker_configuration

  echo "SUCCESS:
  Done! Finished setting up Travis machine.
  "
}

# Prepare the dependencies that the machine need.
# Here I'm just updating the apt references and then
# installing both python and python-pip. This allows
# us to make use of `pip` to fetch the latest `docker-compose`
# later.
# We also upgrade `docker-ce` so that we can get the
# latest docker version which allows us to perform
# image squashing as well as multi-stage builds.
setup_dependencies() {
  echo "INFO:
  Setting up dependencies.
  "

  sudo apt update -y
  sudo apt install realpath python python-pip -y
  sudo apt install --only-upgrade docker-ce -y

  sudo pip install docker-compose || true

  docker info
  docker-compose --version
}

# Tweak the daemon configuration so that we
# can make use of experimental features (like image
# squashing) as well as have a bigger amount of
# concurrent downloads and uploads.
update_docker_configuration() {
  echo "INFO:
  Updating docker configuration
  "

  echo '{
  "experimental": true,
  "storage-driver": "overlay2",
  "max-concurrent-downloads": 50,
  "max-concurrent-uploads": 50
}' | sudo tee /etc/docker/daemon.json
  sudo service docker restart
}

main

With that set, we can move on to the Travis configuration file (.travis.yml):

# make use of vm's 
sudo: 'required'

# have the docker service set up (we'll
# update it later)
services:
  - 'docker'

# prepare the machine before any code
# installation scripts
before_install:
  - './.travis/main.sh'

# first execute the test suite.
# after the test execution is done and didn't
# fail, build the images (if this step fails
# the whole Travis build is considered a failure).
script:
  - 'make test'
  - 'make image'

# only execute the following instructions in
# the case of a success (failing at this point
# won't mark the build as a failure).
# To have `DOCKER_USERNAME` and `DOCKER_PASSWORD`
# filled you need to either use `travis`' cli 
# and then `travis set ..` or go to the travis
# page of your repository and then change the 
# environment in the settings pannel.
after_success:
  - if [[ "$TRAVIS_BRANCH" == "master" ]]; then
      docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD ;
      make push-image ;
    fi

# don't notify me when things fail
notifications:
  email: false

As you might have noticed the whole functionality is behind a Makefile (from the make commands). That’s on purpose to facilitate possible debugging sessions (you’d SSH into the Travis machine and just ./.travis/main.sh, make test to run the whole thing).

The Makefile can be as simple as something like the following:

IMAGE := cirocosta/dind

test:
	true

image:
	docker build -t $(IMAGE) .

push-image:
	docker push $(IMAGE)


.PHONY: image push-image test

If you want an example to check out, see cirocosta/dind.

Closing thoughts

There’s no need to complicate a lot the process of automatically pushing images after a successful test run using travis-ci. I really enjoy the idea of only pushing an image when things are fine as it gives a good guarantee that the code that is being packed as the image will run.

To be even more sure about the wellness of the image you could add an extra suit of integration tests that creates docker containers using the image and then asserts that it works as expected.

If you’d like to have a container-based environment that runs your unit tests and on the side (concurrently) run a build in a VM that prepares your docker images and run integration tests, you can totally do that with Travis-ci - it’s a matter of adapting a previous article I wrote: Concurrently running container-based and VM tests in Travis-CI.

If you’d like to learn more about integrating Travis-ci into your projects or automation in general, make sure you subscribe to the mailing list.

Here are some resources related to the article:

Have a good one!

finis