4 min read

At Pantographe, we use GitLab CE to manage our projects. And we use GitLab CI to run different tests.
For a while we used the official ruby Docker image. But Rails applications need nodejs to be installed for the asset pipeline and since Rails 5.1, for webpacker.
At the beginning we simply installed nodejs package with apt-get install on every job we ran. But it unnecessarily spend time, that's why we create our own ruby Docker image.

This image is just for our own internal usage, so we don't really need to store it into the official Docker repository. And since GitLab comes with its own Docker repository we decided to use it.
So we create a new repository to build and manage our own image.

Structure

We would like to have the ability to manage multiple versions in one projet. So we took a look at the repository of the official ruby Docker image to understand how they did.
So the structure simply looks like this:

├── .gitlab-ci.yml
├── 2.4
│   └── Dockerfile
├── 2.5
│   └── Dockerfile
├── ...
│
└── README.md

For now we don't build variant as slim or alpine but we plan to. And this structure permits us to simply do it in the future by adding subdirectories per variant like following.

├── ...
├── 2.4
│   ├── alpine
│   │   └── Dockerfile
│   ├── slim
│   │   └── Dockerfile
│   └── Dockerfile
└── ...

Docker image

Now it's time to write our Docker image. We don't need to build everything from scratch, we only need to install some more packages. So we simply use the official ruby Docker image and we run our extra commands.

# FROM ruby:2.5 # for our 2.5 image version
FROM ruby:2.5

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    curl \
    wget \
    nodejs \
    zlib1g-dev \
    build-essential \
    libssl-dev \
    libreadline-dev \
    libyaml-dev \
    libsqlite3-dev \
    sqlite3 \
    libxml2-dev \
    libxslt1-dev \
    libcurl4-openssl-dev \
    python-software-properties \
    libffi-dev \
    postgresql-client \
    libpq-dev \
    git-core \
    openssh-client \
    && rm -rf /var/lib/apt/lists/*

RUN gem install pg

Don't forget to install your packages in less lines possible and to purge cache at the end to have image with small size (see: https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#run).

GitLab CI configuration

To make our image usable we need to build and store it. For that work we use GitLab CI to automatise the process.
For this we need to edit the .gitlab-ci.yml file like following:

image: docker:latest

services:
  - docker:dind

variables:
  VARIANT: ""
  DOCKER_DRIVER: overlay2

stages:
  - test
  - release
  - tag

before_script:
  - docker info
  - docker login -u "gitlab-ci-token" -p "$CI_JOB_TOKEN" "$CI_REGISTRY_IMAGE"
  - cd "$VERSION/$VARIANT"
  - image="ci-ruby:${VERSION}${VARIANT:+-${VARIANT}}"
  - registry_image="$CI_REGISTRY_IMAGE:${VERSION}${VARIANT:+-${VARIANT}}"

after_script:
  - docker images

.test_template: &test_template
  stage: test
  script:
    - apk add --no-cache git bash
    - git clone --depth 1 https://github.com/docker-library/official-images.git ~/official-images
    - docker build --pull -t "$image" .
    - ~/official-images/test/run.sh "$image"

.release_template: &release_template
  stage: release
  script:
    - docker build --pull -t "$registry_image" .
    - docker push "$registry_image"
    - full_ruby_version=$(docker run --rm "$registry_image" ruby -e "puts RUBY_VERSION")
    - docker tag "$registry_image" "$CI_REGISTRY_IMAGE:${full_ruby_version}"
    - docker push "$CI_REGISTRY_IMAGE:${full_ruby_version}"
  only:
    - main
  except:
    - schedules

# Ruby 2.4
test:2.4:
  <<: *test_template
  variables:
    VERSION: "2.4"

release:2.4:
  <<: *release_template
  variables:
    VERSION: "2.4"

# Ruby 2.5
test:2.5:
  <<: *test_template
  variables:
    VERSION: "2.5"

release:2.5:
  <<: *release_template
  variables:
    VERSION: "2.5"

# Tags
tag:latest:
  <<: *release_template
  stage: tag
  variables:
    VERSION: "2.5"
  script:
    - docker pull "$registry_image"
    - docker tag "$registry_image" "$CI_REGISTRY_IMAGE:latest"
    - docker push "$CI_REGISTRY_IMAGE:latest"
  dependencies:
    - release:2.5

In this example we test and build 2.4 and 2.5 versions of ruby. We also create a docker tag named latest which targets the 2.5 image version.
We also read the exact version of ruby build to tag it too. For example for 2.5 image we add a tag as 2.5.3.

Why not simply install packages on CI job

Like we explained at the beginning of the article, installing packages on every job waste time unnecessarily.
By installing packages on a Docker image, we optimise our process and save time for these jobs execution. It also allows us to centralise the main dependencies of our projet.

Why don't use existing image?

Using an existant image would do the job but we prefer to have the hand on the image and could easily make evolutions for all our projects without having to edit the .gitlab-ci.yml file of each project we have.

Conclusion

This example we tell you is specific of our usage. But it was a good example to show you how to build a Docker image with GitLab CI.