GitLab CI Docker Implementation Architectures

The integration of Docker within GitLab CI/CD transforms a standard version control repository into a robust automated factory for containerized applications. At its core, this process involves utilizing GitLab CI/CD pipelines to automate the creation, testing, and distribution of Docker images. This capability is accessible across various GitLab tiers, including Free, Premium, and Ultimate, and is supported across GitLab.com (SaaS), GitLab Self-Managed, and GitLab Dedicated offerings. The primary objective is to transition from manual image building on a local machine to a standardized, reproducible pipeline where an application is packaged into an image, validated through automated tests, and pushed to a container registry for deployment.

To achieve this, GitLab CI/CD relies on the GitLab Runner, an agent that executes the jobs defined in the .gitlab-ci.yml configuration. Because Docker is a client-server application, the environment where the job runs must have access to a Docker daemon (dockerd). Since GitLab CI jobs often run inside containers themselves, a specific architectural challenge arises: running a Docker daemon inside a Docker container. This is commonly referred to as Docker-in-Docker (DinD).

Strategic Approaches to Docker Execution in GitLab CI

Depending on the security posture and infrastructure availability, there are three primary methods for executing Docker commands within a GitLab CI pipeline.

The Shell Executor Method

The shell executor allows the GitLab Runner to execute commands directly on the host machine's terminal. This bypasses the need to run a container to build a container.

  • Direct Fact: The runner is configured to use the shell executor.
  • Impact Layer: This removes the abstraction layer of Docker-in-Docker, allowing the gitlab-runner user to call the local Docker Engine installed on the server. It simplifies the setup for small teams or internal servers where the runner has direct root-level or group-level access to the Docker socket.
  • Contextual Layer: This method requires the manual installation of the Docker Engine on the server where the GitLab Runner is hosted.

The registration process for a shell executor involves the following command:

sudo gitlab-runner register -n --url "https://gitlab.com/" --registration-token REGISTRATION_TOKEN --executor shell --description "My Runner"

The Docker-in-Docker (DinD) Method

When using the Docker executor, the job itself is encapsulated in a container. To run Docker commands, a secondary service providing the Docker daemon must be initialized.

  • Direct Fact: Use the docker:dind image as a service in the .gitlab-ci.yml file.
  • Impact Layer: This enables the creation of an isolated environment where each job has its own dedicated Docker daemon. It prevents interference between parallel jobs and ensures a clean state for every build.
  • Contextual Layer: This requires the runner to be configured in "privileged mode" to allow the inner container to manage network and storage resources on the host.

The Kaniko Alternative

For environments where enabling privileged mode on the runner is prohibited due to security policies, Kaniko is the recommended alternative.

  • Direct Fact: Kaniko allows building Docker images without requiring a Docker daemon.
  • Impact Layer: It eliminates the security risks associated with privileged containers, making it the ideal choice for shared runners or strictly regulated cloud environments.
  • Contextual Layer: It serves as a modern replacement for the complex DinD setup while achieving the same result: a pushed image in the registry.

Technical Configuration of Docker-in-Docker (DinD)

Implementing a DinD workflow requires a precise alignment of images, services, and environment variables.

Service and Image Definition

In a .gitlab-ci.yml file, the image keyword defines the environment for the job, while the services keyword defines auxiliary containers that run alongside the job.

  • Direct Fact: The image keyword specifies the Docker CLI tool version (e.g., docker:19.03.1), and the services keyword specifies the daemon (e.g., docker:19.03.1-dind).
  • Impact Layer: Pinning specific versions prevents "breaking changes" from occurring when the latest tag is updated, ensuring that the CI pipeline is deterministic and reproducible.
  • Contextual Layer: The service provides the dockerd server that the CLI tool in the main image communicates with.

Essential Environment Variables for DinD

To ensure the Docker CLI can locate the daemon and operate efficiently, specific variables must be declared:

  • DOCKER_HOST: Defines the network address of the Docker daemon. For example, tcp://dockerdaemon:2375/ or tcp://docker:2375. In Kubernetes-based runners, this is explicitly required.
  • DOCKER_DRIVER: Setting this to overlay2 is recommended for improved performance during image layer creation.
  • DOCKER_TLS_CERTDIR: This specifies where certificates are created (e.g., /certs). Docker creates these automatically on boot, and they are shared between the service and the job container via volume mounts defined in the config.toml.

Comprehensive Pipeline Implementation Example

A functional GitLab CI pipeline for Docker involves a sequence of authentication, building, and pushing.

Example .gitlab-ci.yml Configuration

The following configuration demonstrates a complete DinD implementation:

```yaml
image: docker:19.03.1

services:
- docker:19.03.1-dind

variables:
DOCKERDRIVER: overlay2
DOCKER
TLSCERTDIR: "/certs"
REGISTRY
GROUPPROJECT: $CIREGISTRY/root/gitlab-ci-dind-example

buildjob:
script:
- docker login -u "$CI
REGISTRYUSER" -p "$CIREGISTRYPASSWORD" "$CIREGISTRY"
- docker build -t "$CIREGISTRYIMAGE:dind" .
- docker push "$CIREGISTRYIMAGE:dind"
```

Analysis of the Build Process

  • Direct Fact: The pipeline uses docker login with predefined GitLab CI variables ($CI_REGISTRY_USER, $CI_REGISTRY_PASSWORD).
  • Impact Layer: This allows the pipeline to authenticate securely with the GitLab Container Registry without hardcoding credentials into the source code.
  • Contextual Layer: Once authenticated, the docker build command packages the application based on the Dockerfile in the root directory, and docker push uploads the artifact for later deployment.

Dockerfile Construction and Integration

The quality of the CI output depends on the Dockerfile. A well-structured Dockerfile ensures that images are lean and secure.

Advanced Dockerfile Example

Consider a Python-based application requiring a specific environment:

```dockerfile

Make the base image configurable

ARG BASEIMAGE=python:3.7
FROM ${BASE
IMAGE}
USER root
RUN apt-get -qq -y update && \
apt-get -qq -y upgrade && \
apt-get -y autoclean && \
apt-get -y autoremove && \
rm -rf /var/lib/apt/lists/*

Create user "docker"

RUN useradd -m docker && \
cp /root/.bashrc /home/docker/ && \
mkdir /home/docker/data && \
chown -R --from=root docker /home/docker
ENV HOME /home/docker
WORKDIR ${HOME}/data
USER docker
COPY entrypoint.sh $HOME/entrypoint.sh
ENTRYPOINT ["/bin/bash", "/home/docker/entrypoint.sh"]
CMD ["Docker"]
```

  • Direct Fact: The use of ARG BASE_IMAGE allows the base version to be changed without modifying the entire file.
  • Impact Layer: By cleaning up apt lists (rm -rf /var/lib/apt/lists/*), the final image size is significantly reduced, leading to faster pull times and lower storage costs.
  • Contextual Layer: Switching from USER root to USER docker implements the principle of least privilege, preventing the application from running as root inside the container.

Entrypoint Scripting

The entrypoint.sh script acts as the primary gateway for the container:

```bash

!/usr/bin/env bash

set -e
function main() {
if [[ $# -eq 0 ]]; then
printf "Welcome to the Dockerized environment\n"
fi
}
main "$@"
```

Runner Registration and Image Requirements

To support these workflows, the GitLab Runner must be correctly provisioned.

Registering a Docker Executor

When registering a runner for Docker use, one can use a template file to pre-define services.

Example template (/tmp/test-config.template.toml):

toml [[runners]] [runners.docker] [[runners.docker.services]] name = "postgres:latest" [[runners.docker.services]] name = "mysql:latest"

The registration command for this configuration is:

sudo gitlab-runner register --url "https://gitlab.example.com/" --token "$RUNNER_TOKEN" --description "docker-ruby:2.6" --executor "docker" --template-config /tmp/test-config.template.toml --docker-image ruby:3.3

Mandatory Image Specifications

For any image used as a CI/CD job executor, certain system tools must be present to ensure the GitLab Runner can communicate with the container.

  • Direct Fact: Every image must have sh, bash, and grep installed.
  • Impact Layer: If these utilities are missing, the runner will fail to execute the script sections of the .gitlab-ci.yml file, resulting in a "job failed" status.
  • Contextual Layer: This explains why many CI images use slim or alpine versions but must still verify the presence of these core shell utilities.

Operational Workflow: From Build to Execution

The full lifecycle of a Docker image in GitLab CI follows a specific linear path.

Comparison of CI/CD Workflow Stages

Stage Action Tool/Command Purpose
Preparation Clone Repository git clone Bring code to the runner
Configuration Define Pipeline .gitlab-ci.yml Orchestrate the build
Construction Build Image docker build Create a portable artifact
Distribution Push to Registry docker push Store image in GitLab Registry
Deployment Pull and Run docker pull Execute image in target env

Pulling and Verifying Images

Once the pipeline completes, the image can be retrieved from the GitLab registry using the full registry path.

Example command to pull and run a Python 3.8 image:

docker pull gitlab-registry.cern.ch/<user name>/build-with-ci-example:py-3.8
docker run --rm -ti gitlab-registry.cern.ch/<user name>/build-with-ci-example:py-3.8 python3 --version

Conclusion

The orchestration of Docker within GitLab CI/CD represents a convergence of virtualization and automation. By leveraging the Docker-in-Docker (DinD) pattern or the Shell executor, developers can create a seamless pipeline that transforms source code into a deployable container image. The technical requirement for privileged mode in DinD highlights a critical trade-off between flexibility and security, a gap that tools like Kaniko are designed to bridge.

The efficiency of this system relies on the precise configuration of the .gitlab-ci.yml file, specifically the alignment of image and services keywords and the correct assignment of DOCKER_HOST and DOCKER_TLS_CERTDIR. Furthermore, the adherence to image best practices—such as utilizing non-root users and cleaning up package manager caches—directly impacts the performance and security of the resulting artifacts. When these elements are synchronized, GitLab CI provides a powerful, scalable engine for modern DevOps, allowing for the parallel construction and distribution of complex microservices architectures.

Sources

  1. Using Docker to build Docker images
  2. GitLab CI DinD Example
  3. Building Docker Images in GitLab CI
  4. Introduction to Docker: Build with CI
  5. Using Docker Images in GitLab CI

Related Posts