GitLab CI: How to Build Docker Images in Docker

Page content

One of the most common use case is to build a Docker image with Gitlab. In this post I will show you how to set up Docker builds in CI.

Shell Executor

The easiest way to build a docker image is by using the Shell executor. For this you need a standard linux based Gitlab Runner with Docker installed on it. Then the gitlab runner user needs to be in the docker group to execute docker commands.

To build a Docker image you mast store the Dockerfile in your repository. Here is the .gitlab-ci.yaml for this build:

stages:
  - build

docker_build:
  stage: build
  script:
    - docker build -t example.com/example-image:latest .
    - docker push example.com/example-image:latest
  tags: 
    - docker

To push an image to a docker registry you need to authenticate. Luckily Gitlab has it’s own docker registry and automatically gives access to the runners. So you can use the built-in variables for this:

stages:
  - build

docker_build:
  stage: build
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/example-image:latest .
    - docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/example-image:latest
  tags: 
    - docker

The next problem is to identify your images, which image belongs to which build.

stages:
  - build

docker_build:
  stage: build
  script: |
    docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    if [[ "$CI_BUILD_REF_NAME" == "master" ]]; then
      DOCKER_TAG="latest"
    else
      DOCKER_TAG=$CI_COMMIT_REF_SLUG
    fi
    docker build -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/example-image:latest .
    docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/example-image:latest
    echo $DOCKER_TAG    
  tags: 
    - docker

It is a best practice to use the commit hash as the tag. For release you can use the git release tag as tag:

stages:
  - build

docker_build:
  stage: build
  script: |
    docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    if [[ "$CI_BUILD_REF_NAME" == "master" ]]; then
      DOCKER_TAG="latest"
    else
      DOCKER_TAG=$CI_COMMIT_SHA
    fi
    docker build -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/example-image:$DOCKER_TAG .
    docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/example-image:$DOCKER_TAG
    echo $DOCKER_TAG    
  tags: 
    - docker

docker_release:
  stage: build
  script: |
    docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    docker build -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/example-image:$CI_COMMIT_TAG .
    docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/example-image:$CI_COMMIT_TAG
    echo $CI_COMMIT_TAG    
  tags: 
    - docker
  only:
    - master
    - tags
    - /^release-.*$/

Usually you build your app in a separate job dan put your artifact in the docker image, but multistage docker build you can do thi in tha same job. For this you need t edit your Dockerfile to have an application build.

FROM node:8 AS builder

ADD package*.json /app/
WORKDIR /app
RUN npm install

ADD . /app
RUN npm run-script build

FROM nginx
COPY --from=builder /app/dist/* /usr/share/nginx/html/
COPY --from=builder /app/dist/assets /usr/share/nginx/html/assets
stages:
  - build

variables:
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_PIPELINE_ID

before_script:
  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

docker_build:
  stage: build
  script:
    # Here we try to download a Docker image with the :builder tag (which contains all the layers from the last build)
    - docker pull $CI_REGISTRY_IMAGE:builder || true
    # It will run the first part of the Dockerfile. Also we tell Docker to use the cache layers from the previous build.
    - docker build --pull --cache-from $CI_REGISTRY_IMAGE:builder --target builder -t $CI_REGISTRY_IMAGE:builder .
    - docker build --pull --cache-from $CI_REGISTRY_IMAGE:builder --cache-from $IMAGE_TAG -t $IMAGE_TAG -t $CI_REGISTRY_IMAGE:latest .
    - docker push $CI_REGISTRY_IMAGE:builder
    - docker push $CI_REGISTRY_IMAGE:latest
    - docker push $IMAGE_TAG
  tags: 
    - docker

Building With the Docker Executor

You can use docker as an environment to run all the scripts to provide a completely clean environment for each job. For this you need to use gitlab runner in a so called Docker-in-Docker (DinD) mode. In a normal docker environment yo can not use docker commands in the container. When you use the docker cli it needs to connect to the docker engine unix socket. To do this you need to mount the docker socket in the container and run as privileged mode.

Register your runner in DinD mode:

sudo gitlab-runner register -n 
  --url https://example.com 
  --registration-token $GITLAB_REGISTRATION_TOKEN 
  --executor docker 
  --description "Docker Runner" 
  --docker-image "docker:20.10" 
  --docker-volumes "/certs/client" 
  --docker-privileged

When you configure CI/CD, you specify an image, which is used to create the container where your jobs run. To specify this image, you use the image keyword. ou can specify an additional image by using the services keyword. Within your CI pipeline, add the docker:dind image as a service. This gives a separate image to communicate with the docker engine.

services:
  - docker:dind

docker_build:
  stage: build
  image: docker:latest
  script:
    - docker build -t example-image:latest .