Using Docker with Travis Continuous Integration

 Sept. 16, 2018     0 comments

Note 2: This article was written before GitHub Actions became publicly available so now it has more of a historic value.

Note: This is a re-worked version of my previous article. I it has been updated based on practical experience and to reflect recent changes in Travis CI.

GitHub supports several cloud-based continuous integration services. One of them is Travis CI that allows to run automated testing and deployment tasks on Linux Ubuntu and macOS. Unfortunately, their Ubuntu environment is quite dated — currently they use Ubuntu 14.04 LTS (Trusty Tahr) or Ubuntu 16.04 (Xenial Xerus) while the current LTS release (at the moment of this writing) is Ubuntu 18.04 LTS (Bionic Beaver), so some of the recent language and tooling versions may not be available in Travis CI build environment. For example, Trusty environment does not support Python 3.7, and Xenial environment (which is considered experimental at the moment of this writing) does not support Python 3.4, so you cannot run tests for Python 3.4-3.7 in one environment without writing a complex build matrix. Users already asked Travis CI team about updating their Linux build environments to the current LTS release but Travis CI team's reply was basically "use Docker if you want anything else".

Unfortunately, Travis CI documentation about using Docker is not very clear. Maybe it makes sense for Ruby programmers but I'm more a Python guy and not familiar with Ruby at all. That is why I decided to provide an example based on Python language. Despite that, my example demonstrates a generic way to execute commands inside a Docker container so it can be easily adapted to other languages. It uses the latest Ubuntu LTS Docker image to test a simple Python program using tox test runner, but you can use any Docker image and programming language you like. This example assumes that you are familiar with Travis CI and its YAML-based configuration files, and also with Linux shell in general.

Here's my Travis CI config for testing a Python program with the latest Ubuntu Docker image:

 Click to show
# It is not really needed, other than for showing a correct language tag in Travis CI build log.
language: python

sudo: required

services:
  - docker

env: SH="docker exec -t ubuntu-test bash -c"

before_install:
  - docker run -d --name ubuntu-test -e LC_ALL="en_US.UTF-8" -e LANG="en_US.UTF-8" -v $(pwd):/travis -w /travis ubuntu:latest tail -f /dev/null
  - docker ps

install:
  - $SH "apt-get update"
  - $SH "apt-get install -y locales"
  - $SH "locale-gen en_US.UTF-8"
  - $SH "apt-get install -y software-properties-common python python3 python3-pip"
  - $SH "python3 -m pip install -q --upgrade pip"
  - $SH "python3 -m pip install -q tox"
  - $SH "add-apt-repository -y ppa:deadsnakes/ppa"
  - $SH "apt-get update"
  - $SH "apt-get install -y python3.3 python3.4 python3.5 python3.7"

script:
  - $SH "tox"

notifications:
    email: false

Let's focus on Docker-specific items of this config. Line 7 specifies that Docker is used in this build.

Line 9 defines a shortcut ($SH) for docker exec command that allows to execute shell commands inside a Docker container.

-t option allocates a pseudo-TTY for the current command so you can view output or error messages, if any, in Travis CI build log.

ubuntu-test is the name of a container we send commands to.

bash -c means that we will execute the following string in bash shell.

docker run command on line 12 pulls the necessary image (the latest Ubuntu LTS in this case) from Docker repository and starts a docker container, that is, an isolated running "virtual machine" based on the Docker image.

-d option tells that our image will run in detached or background mode.

--name option sets the name of our container ("ubuntu-test")  so we can address it later (note $SH command shortcut).

-e options allow to specify environment variables to be used inside the container. In this case we set the default character encoding for bash console. For those settings to work we need to install the respective encoding which is done in lines 17 and 18.

-v option maps the current working directory ($(pwd)) into our Docker container under /travis directory so we can access our build files from within the container.

-w option sets this directory as the default working directory inside the container.

ubuntu:latest is the name of our Docker image that our build container is based on.

The problem with docker run command is that our container immediately stops if we do not provide it with any commands, so we need to keep it running somehow. And this is what the command tail -f /dev/null is used for — it blocks the shell inside the container and prevents it from stopping, so we can send our commands to it.

docker ps command on line 13 prints the list of running containers. It is optional and used only for diagnostic purposes.

In install section we use $SH shortcut defined earlier to install our build prerequisites, using build matrix variables, if necessary. In build section we use our $SH shortcut again to run build commands. In this case we launch only one tox command to run our Python test suite, but for different languages and/or scenarios we can run a series of commands. For example, for C/C++ build section might look like this:

build:
  - $SH "cmake ."
  - $SH "make"
  - $SH "make test"

If one of the commands launched with $SH shortcut fails, docker exec command returns a non-zero error code so the subsequent commands won't run and this Travis CI build will be marked as failed. Using $SH shortcut to launch our shell commands line by line makes it easy to see which command failed in the Travis CI build log.

Because our Travis build directory is mounted inside our Docker container, all artifacts generated during build phase are available for subsequent phases, e.g. for deployment via various providers available in Travis CI.

The example project for this article that uses Docker inside Travis CI to test a simple Python program is available in my GitHub repository.

  Continuous IntegrationDockerGitHubPython