GitLab CI: How to Build Docker Images in Docker
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:
rss_ignore: true
- 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:
rss_ignore: true
- 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:
rss_ignore: true
- 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:
rss_ignore: true
- 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:
rss_ignore: true
- 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:
rss_ignore: true
- 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 .