A guide to automated Docker deployments w/ GitLab CI

Because shipping with Docker needs a nautical theme.
This article assumes you are using (or interested in using) GitLab and GitLab CI. While the following explains how to deploy Docker containers, it can easily be adjusted to accommodate any type of project. Feel free to post any questions in the comments section below.

Objectives

  1. Once the production branch (or any branch we’re interested in) is pushed, have the runner build our application’s Docker container.
  2. Once the container is built, push to our project’s GitLab private container registry.
  3. Deploy the container to our production server using the (free) Stackahoy command-line tool. As a disclaimer, we built Stackahoy and have been using it internally for a few years.

Step 1: Create a runner.

GitLab CI runners are decoupled from the core GitLab CI instance and serve as the workhorses for testing, building, and deploying an application. We are using GitLab’s Multi-runner on a small Google Compute Engine instance.

Follow the steps here for installing the runner in your desired environment, then run:

# Make sure to replace REG_TOKEN and/or the 
# --url to match your environment.
sudo gitlab-ci-multi-runner register \
-n \
--url https://path-to-github.com/ci \
--tag-list "docker" \
--registration-token REG_TOKEN \
--executor docker \
--description "Docker Runner" \
--docker-image "docker:latest" \
--docker-volumes /var/run/docker.sock:/var/run/docker.sock

As of writing this article, there are three ways of using the “docker” command within our runner container. You can read about them here. We chose to volume the docker.sock file.

Step 2: Configure gitlab-ci.yml.

The gitlab-ci.yml file should be placed in the root of the project. This contains instructions used by GitLab CI.

.gitlab-ci.yml:

variables:
REGISTRY: your-gitlab-server:4567
REPO_ID: 574535091c632d0d00f646cc
stages:
- build
- deploy
build:
image: docker:latest
stage: build
script:
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $REGISTRY
- docker build -t $REGISTRY/<PROJECT-GROUP>/<PROJECT-NAME> .
- docker push $REGISTRY/<PROJECT-GROUP>/<PROJECT-NAME>
only:
- production
tags:
- docker
deploy:
image: stackahoy/stackahoy-cli
stage: deploy
script:
- stackahoy deploy -t $STACKAHOY_TOKEN -b production -r $REPO_ID --skip-delivery
only:
- production
tags:
- docker

Let’s break the file down.

variables:
REGISTRY: your-gitlab-server:4567
REPO_ID: 574535091c632d0d00f646cc

Here we define two variables which are used later in the file. This is simply to make the file more readable and reusable for other projects. REGISTRY is the URL for the private GitLab container registry. REPO_ID is the Stackahoy repository ID — you will retrieve this from the repository URL for the landing page within Stackahoy — https://stackahoy.io/group/<repo-id>/.

stages:
- build
- deploy

The stages property simply allows you to define the order in which the jobs should be executed. In this instance, the jobs are “build” and “deploy”. You could name these anything you’d like. Generally, you’d have a “test” job as well to handle your functional and unit tests and maybe a specific job for building a staging container.

build:
image: docker:latest
stage: build
script:
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $REGISTRY
- docker build -t $REGISTRY/<PROJECT-GROUP>/<PROJECT-NAME> .
- docker push $REGISTRY/<PROJECT-GROUP>/<PROJECT-NAME>
only:
- production
tags:
- docker

The “build” job does two things. First, it will build the Docker container. Second, it will push the container to the private GitLab registry.

The last thing I’ll note about this snippet is the user that docker logs in as — gitlab-ci-token. This is a special user which is meant to be used with the variable $CI_BUILD_TOKEN. This variable will be defined on any GitLab CI build. This is much cleaner and safer than using your personal credentials for GitLab. The <PROJECT-GROUP>/<PROJECT-NAME> should be set based on the name of the project’s group and name, respectively.

deploy:
image: ubuntu:latest
stage: deploy
script:
- apt-get update
- apt-get install -y -qq npm curl
- npm install -g n
- n stable
- npm install -g stackahoy
- stackahoy deploy -t $STACKAHOY_TOKEN -b production -r $REPO_ID --skip-delivery
only:
- production
tags:
- docker

Update (July 2017): Now you can use the new Stackahoy CLI docker container to speed this up. Doing so would look like this:

deploy:
image: stackahoy/stackahoy-cli
stage: deploy
script:
- stackahoy deploy -t $STACKAHOY_TOKEN -b production -r $REPO_ID --skip-delivery
only:
- production
tags:
- docker

Finally, we’re at the deployment. In order to deploy, we can use Stackahoy which actually handles the secure transfer of files from the repository to the staging or production server. In our case, we’re not transferring files (note the --skip-delivery flag) and only running the docker run commands on the server since the container is already built.

Stackahoy Repository Configuration Page

One really cool feature in GitLab is it allows us to define variables in the project’s settings so we don’t have to commit them to the repository. This is where the $STACKAHOY_TOKEN variable is being set. This special token is obtained from the Stackahoy dashboard and allows the stackahoy command to deploy.

GitLab Project Admin — Variables

Step 3: Deploy.

Stackahoy takes care of the work on the destination server. This work includes:

  • Syncing files (taking .gitignore file in account) to our server(s).
  • Running/restarting Docker containers.
  • Delivering static configuration files.
  • Building assets, clearing cache, db migrations, ect.

For this setup, we’ll be starting up a MongoDB container using the public Mongo image from the Docker hub and running the container we just built in the “build” stage. The Stackahoy post-commands will look like the following:

# Start up mongo container.
docker run --name my-mongo -d mongo
# Login to private registry.
docker login -u gitlab-ci-token -p [token-here] [url-here]
# Run app and link mongo.
docker run \
--name my-app \
-p 80:80 \
-e "NODE_ENV=production" \
--link my-mongo:mongo \
-d [url-here]

The “--skip-delivery” flag in the gitlab-ci.yml file tells Stackahoy to only run these commands and not to worry about file syncing which keeps deployments lean and to the point.

Conclusion

A real world application will likely have a few more jobs such as various testing and at least one more container build in the .gitlab-ci.yml file, but this should provide a solid starting point for your project.

Useful links