GitLab CI/CD Docker Image Orchestration and Automated Build Pipelines

The integration of Docker into GitLab CI/CD pipelines represents a fundamental shift in how modern software is packaged, tested, and deployed. By leveraging a .gitlab-ci.yml configuration, developers can transition from manual image creation to a fully automated lifecycle where code commits trigger the construction of OCI-compliant containers. This process is not merely about running a command; it involves a complex interplay between the GitLab Runner, the Docker daemon, and the container registry. To achieve a successful build, one must navigate the specific architectural requirements of the executor being used, whether it is the shell executor or the Docker executor. The core objective is to transform a Dockerfile—a blueprint for the environment—into a versioned image stored in a registry, which can then be pulled and executed across various environments, from staging to production.

Architectural Implementations for Docker Builds

Building Docker images within a CI/CD pipeline requires a mechanism to execute Docker commands. Since GitLab CI jobs typically run inside containers themselves, the challenge is providing a Docker daemon (the server that does the actual work) to the Docker CLI (the client that sends commands).

Docker-in-Docker (DinD)

The most common approach for Docker-based executors is Docker-in-Docker. In this architecture, the docker CLI does not perform the build work but instead communicates with a daemon called dockerd.

  • Direct Fact: The docker:dind image provides a running Docker daemon.
  • Impact Layer: This allows users to execute docker build, docker push, and docker run commands inside a job that is already running as a container.
  • Contextual Layer: This is implemented by defining docker:dind as a service within the .gitlab-ci.yml file, which creates a sidecar container to handle the daemon requests.

The configuration for a DinD build requires specific environment variables to ensure the CLI can locate the daemon.

  • Direct Fact: The DOCKER_HOST variable must be set to tcp://dockerdaemon:2375/ when using a service alias.
  • Impact Layer: Without this variable, the Docker CLI will attempt to connect to a local Unix socket, which does not exist in the job container, leading to a connection failure.
  • Contextual Layer: This variable works in tandem with the services keyword to route traffic from the main job container to the dockerdaemon alias.

The Shell Executor Approach

As an alternative to DinD, the shell executor allows the runner to execute commands directly on the host machine's shell.

  • Direct Fact: The shell executor requires the installation of Docker Engine on the server where the GitLab Runner is installed.
  • Impact Layer: This removes the need for privileged containers or sidecar services, as the job uses the host's native Docker daemon.
  • Contextual Layer: This is a viable path for those who want to avoid the security implications of privileged mode in the Docker executor.

To register a runner with the shell executor, the following command is utilized:

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

Advanced Build Strategies and Daemon Alternatives

For environments where security policies forbid the use of privileged mode, GitLab provides several alternatives to the standard Docker daemon.

Buildah and Rootless BuildKit

Buildah is a tool designed to build OCI-compliant images without requiring a running Docker daemon.

  • Direct Fact: Buildah can be used with the quay.io/buildah/stable image.
  • Impact Layer: This enables the creation of images in non-privileged environments, significantly reducing the attack surface of the CI runner.
  • Contextual Layer: Buildah serves as a replacement for the docker build and docker push workflow, integrating directly into the build stage of the pipeline.

An example configuration for Buildah is as follows:

yaml build: stage: build image: quay.io/buildah/stable variables: # Use vfs with buildah

Storage Driver Optimization

The performance of Docker builds is heavily influenced by the storage driver. The OverlayFS driver is generally recommended for better performance and efficiency.

  • Direct Fact: The DOCKER_DRIVER variable can be set to overlay2 in the .gitlab-ci.yml file.
  • Impact Layer: Using overlay2 reduces the overhead of image layers, leading to faster build times and lower disk usage.
  • Contextual Layer: This configuration can be applied globally via the runner's config.toml or individually per project.

To enable the OverlayFS module on Ubuntu systems, the following steps are required:

bash sudo modprobe overlay

To ensure the module persists after a reboot, the term overlay must be added to the /etc/modules file.

Comprehensive .gitlab-ci.yml Configuration Examples

The .gitlab-ci.yml file is the heart of the automation process. It defines the stages, the images used for execution, and the specific scripts to be run.

Fundamental Structure

A basic pipeline for building and deploying an image typically consists of a build stage and a deploy stage.

  • Direct Fact: The image keyword specifies the Docker image the executor uses to run the job.
  • Impact Layer: Choosing the correct image (e.g., docker:latest) ensures that the necessary binaries like sh, bash, and grep are available.
  • Contextual Layer: If the image keyword is omitted, the runner uses a default image, which may lack the tools required for Docker operations.

Below is a structural example of a pipeline:

```yaml
image: docker:latest
services:
- docker:dind
stages:
- build
- deploy

build:
stage: build
script:
- docker compose build

deploy:
stage: deploy
script:
- docker compose up -d
```

Integration with GitLab Container Registry

The final step of a build pipeline is typically pushing the image to the GitLab Registry so it can be utilized by other jobs or deployed to production.

  • Direct Fact: Images are pushed using the full registry name, such as gitlab-registry.cern.ch/<user name>/build-with-ci-example:py-3.8.
  • Impact Layer: This allows for versioning and central storage of images, ensuring that the exact same image tested in CI is the one deployed to the server.
  • Contextual Layer: This process links the build stage (where the image is created) to the deployment stage (where the image is pulled).

To pull and run an image from the registry, the following commands are used:

bash 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

Dockerfile Design and Implementation

The quality of the CI output depends on the Dockerfile. A well-constructed Dockerfile ensures that the resulting image is lean and secure.

Configurable Base Images

Using arguments allows the image to be flexible during the build process.

  • Direct Fact: The ARG instruction allows the base image to be configurable, such as ARG BASE_IMAGE=python:3.7.
  • Impact Layer: This allows developers to switch the underlying OS or language version without modifying the core Dockerfile logic.
  • Contextual Layer: This is particularly useful in CI pipelines where different versions of an image may need to be built in parallel.

Implementation of the Entrypoint Script

The ENTRYPOINT instruction determines what happens when the container starts.

  • Direct Fact: A Bash script (e.g., entrypoint.sh) can be used as the entry point to handle initialization logic.
  • Impact Layer: This ensures that the environment is correctly set up (e.g., setting permissions or checking configurations) before the main application starts.
  • Contextual Layer: The ENTRYPOINT is combined with the CMD instruction to provide a default command that can be overridden by the user.

Example Dockerfile Construction:

```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"]
```

GitLab Runner Configuration and Service Management

The runner's configuration determines how services are handled and how the executor interacts with the host.

Using the Docker Executor

To use the Docker executor, the runner must be registered specifically for that purpose.

  • Direct Fact: A template configuration file can be used to supply default services, such as postgres:latest or mysql:latest.
  • Impact Layer: This allows developers to spin up database dependencies automatically for integration testing without defining them in every single .gitlab-ci.yml file.
  • Contextual Layer: These services are defined in the config.toml file of the runner.

Example of creating a template configuration:

bash cat > /tmp/test-config.template.toml << EOF [[runners]] [runners.docker] [[runners.docker.services]] name = "postgres:latest" [[runners.docker.services]] name = "mysql:latest" EOF

Then, register the runner using this template:

bash 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

Technical Specifications Comparison

The following table compares the different methods for building Docker images within GitLab CI.

Method Executor Privileged Mode Daemon Requirement Primary Use Case
Docker-in-Docker Docker Required docker:dind service Standard Docker builds
Shell Executor Shell N/A Host Docker Engine High performance/Direct access
Buildah Docker/K8s Not Required None Rootless/Secure builds
BuildKit Docker Not Required Rootless BuildKit Modern OCI image construction

Detailed Analysis of Pipeline Workflow

The transition from code to a running container involves several discrete steps that must be synchronized.

First, the developer creates a feature branch and commits a Dockerfile and an entrypoint.sh script. Upon pushing these changes to GitLab, the CI pipeline is triggered. The build stage initializes a job using the docker:latest image and the docker:dind service. The DOCKER_HOST variable directs the CLI to the daemon, allowing the docker build command to execute.

Once the image is built, it is tagged and pushed to the GitLab Container Registry. This is a critical step because the registry acts as the bridge between the build environment and the deployment environment. In the deploy stage, the pipeline can use docker compose or a similar tool to pull the image from the registry and start the container on a target server.

The use of a non-root user within the Dockerfile (e.g., useradd -m docker) is a security best practice that prevents the containerized application from having root privileges on the host system. Furthermore, cleaning up the apt cache using rm -rf /var/lib/apt/lists/* reduces the final image size, which in turn reduces the time required to push and pull the image across the network.

Conclusion

Implementing a Docker build pipeline in GitLab CI/CD requires a nuanced understanding of the relationship between the GitLab Runner, the Docker daemon, and the container registry. The choice between Docker-in-Docker and alternatives like Buildah is primarily driven by security requirements and the available infrastructure. While DinD offers a familiar environment, it necessitates privileged mode, which may be a security risk in shared environments. Conversely, the shell executor provides direct access but requires manual installation of Docker on the host.

The integration of DOCKER_DRIVER=overlay2 and the use of configurable base images through ARG demonstrate a professional approach to optimizing both speed and flexibility. By automating the sequence of building, pushing, and pulling images, organizations can achieve a high level of consistency in their deployments. The use of entrypoint scripts further ensures that the containerized environment is initialized correctly every time. Ultimately, the success of these pipelines relies on a strict adherence to OCI standards and a robust configuration of the .gitlab-ci.yml file, transforming a simple build process into a scalable, industrial-grade deployment engine.

Sources

  1. PythonSpeed - GitLab Build Docker Image
  2. Matthew Feickert - Intro to Docker: Build with CI
  3. GitLab Documentation - Using Docker to build Docker images
  4. GitLab Forum - How to write a gitlab-ci.yml file
  5. GitLab Documentation - Using Docker images

Related Posts