Orchestrating Container Image Construction via GitLab CI/CD

The process of integrating Docker image construction into a GitLab CI/CD pipeline represents a critical intersection of continuous integration and containerization. At its core, this workflow allows developers to transform source code into a portable, immutable artifact—the Docker image—which is then pushed to a registry for deployment. However, because GitLab CI jobs typically execute within their own isolated containers, a fundamental architectural challenge arises: the need for a Docker daemon to execute the docker build and docker push commands. This requirement necessitates specific configurations, ranging from the use of Docker-in-Docker (DinD) and service aliases to the adoption of daemonless alternatives like Kaniko or Podman.

Achieving a successful build requires a precise orchestration of the .gitlab-ci.yml configuration, the selection of the appropriate runner executor, and the correct authentication mechanisms for the target container registry. Whether operating on a hosted GitLab.com instance or a self-managed installation across Free, Premium, or Ultimate tiers, the underlying logic remains consistent: the pipeline must provide a bridge between the GitLab Runner and a functional Docker engine.

The Architecture of Docker-in-Docker (DinD)

In a standard local environment, the docker command-line interface (CLI) acts as a client that communicates with a background process known as dockerd, or the Docker daemon. The CLI does not perform the actual work of building images or managing containers; instead, it sends instructions to the daemon via a socket or a TCP network connection. Within the ephemeral environment of a GitLab CI job, which is itself often a container, the dockerd process is not present by default.

To resolve this, GitLab utilizes a pattern known as Docker-in-Docker (DinD). In this setup, a secondary container running the Docker daemon is launched as a service alongside the job container.

The Role of Services and Aliases

The .gitlab-ci.yml file allows the definition of services, which are additional containers linked to the primary job image. When docker:dind is specified as a service, GitLab starts a container that provides the necessary Docker daemon.

A critical component of this configuration is the service alias. By assigning an alias, such as docker or dockerdaemon, the developer creates a predictable network hostname that the CLI can target.

For example, a configuration might look as follows:

yaml build: image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:24.0.5-cli services: - name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:24.0.5-dind alias: docker stage: build script: - docker build -t my-docker-image . - docker run my-docker-image /script/to/run/tests

The impact of omitting the service alias is immediate and catastrophic to the pipeline's success. Without a defined alias, the CLI cannot resolve the address of the daemon, resulting in a network lookup failure. The typical error presented in the logs is:

error during connect: Get http://docker:2376/v1.39/info: dial tcp: lookup docker on 192.168.0.1:53: no such host

This failure occurs because the DNS within the CI environment cannot map the request to the DinD service container, effectively severing the connection between the client and the engine.

Environment Variables and DOCKER_HOST

In some configurations, simply adding the service is insufficient. The Docker CLI must be explicitly told where to find the server. This is achieved via the DOCKER_HOST environment variable. If a service is aliased as dockerdaemon, the variable must be mapped to the corresponding TCP address.

The following configuration illustrates this requirement:

yaml dind-build: variables: DOCKER_HOST: tcp://dockerdaemon:2375/ services: - name: docker:dind alias: dockerdaemon

By setting DOCKER_HOST, the developer ensures that every docker command issued in the script section is routed through the network to the dockerdaemon container rather than attempting to find a local Unix socket, which does not exist in the job container.

Container Registry Authentication and Management

Before an image can be pushed to a registry, the pipeline must establish a secure session. GitLab provides built-in variables to facilitate this, ensuring that credentials are not hardcoded into the .gitlab-ci.yml file.

Authentication Workflow

The standard procedure for authentication involves using the docker login command. To maintain security, it is recommended to pipe the password into the command via stdin to avoid leaking credentials in the process list.

The typical sequence for building and pushing an image is as follows:

yaml build: image: docker:24.0.5-cli stage: build services: - docker:24.0.5-dind script: - echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin - docker build -t $CI_REGISTRY/group/project/image:latest . - docker push $CI_REGISTRY/group/project/image:latest

The use of $CI_REGISTRY, $CI_REGISTRY_USER, and $CI_REGISTRY_PASSWORD allows the pipeline to work across different projects and environments without modification.

Optimization Strategies for Image Builds

To ensure the integrity and efficiency of the build process, several strategic commands should be employed:

  • Use docker build --pull: This command forces the builder to attempt to pull a newer version of the base image. This prevents the pipeline from using a stale, cached version of a base image (e.g., node:latest), ensuring that security patches and updates are included in the final build.
  • Explicit docker pull: When using multiple runners that may cache images locally, performing an explicit pull before a docker run command ensures the most recent version of the image is being tested.
  • Unique Tagging: Utilizing the Git SHA in the image tag prevents the "stale image" problem. Since every commit has a unique SHA, each job produces a unique image, eliminating the risk of accidentally deploying a previous version of the latest tag.

Alternative Build Methodologies

While Docker-in-Docker is the traditional approach, it is not the only method available. Depending on the security requirements and the infrastructure, other tools may be more appropriate.

Kaniko and Podman

GitLab has increasingly recommended the use of Kaniko for building images. Kaniko is a tool that performs the build process in a daemonless way, meaning it does not require a Docker daemon to run. This removes the need for privileged containers, which is a significant security advantage.

Podman serves as another alternative. Podman is a reimplemented version of Docker that is also daemonless. It provides a similar CLI experience to Docker but avoids the architectural complexities and security risks associated with running a daemon inside a container.

Evolution of Build Pipelines

Historically, before the advent of Docker multi-stage builds and advanced GitLab job artifacts, developers faced significant hurdles in creating clean production images. Early implementations often relied on the GitLab cache feature to transfer compiled binaries from a build stage into a subsequent containerization stage. The binaries would be cached and then referenced in the Dockerfile during the docker build process. Multi-stage builds have largely deprecated this need by allowing the compilation and the final image creation to happen within a single Dockerfile using multiple FROM instructions.

Windows-Based Build Challenges

Building Docker images on Windows environments introduces unique constraints, particularly concerning the GitLab Runner and the version of the Windows Server OS.

Executor Limitations

A common issue for users on Windows Server 2012 is that the gitlab-runner does not support the Docker executor on that specific version. When using the shell executor on Windows Server 2012, the build process cannot simply "pull" a DinD container as it does on Linux.

For users in this environment, the following paths are available:

  • OS Upgrade: Updating the server to Windows Server 2016, 1809, or 1903. These versions support Docker, allowing for the execution of docker build --no-cache --pull . directly on the host.
  • Dedicated Build VM: Deploying a separate VM running a supported version (e.g., 2016 LTS or the half-annual 1809/1903 versions) and tagging the GitLab Runner to route build jobs specifically to that machine.
  • External Mirroring: Using the GitLab-EE mirror feature to push code to GitHub, which can then trigger automated builds via Docker Hub.

Troubleshooting Common Failures

A frequent failure point in GitLab CI Docker builds is the "Cannot connect to the Docker daemon" error. This typically manifests as:

Cannot connect to the Docker daemon at tcp://docker:2375. Is the docker daemon running?

This error usually stems from one of three issues:

  1. Missing Service: The services section is omitted from the .gitlab-ci.yml, meaning no Docker daemon is actually running.
  2. Hostname Mismatch: The DOCKER_HOST variable is pointing to tcp://docker:2375, but the service alias is either different or not defined.
  3. Version Incompatibility: Using a CLI image (e.g., docker:24.0.5-cli) that is incompatible with the DinD service version.

To resolve this, the developer must ensure the services definition matches the DOCKER_HOST target. If the service is named docker:dind, the default hostname is docker. If the service is aliased as dockerdaemon, the host must be updated to tcp://dockerdaemon:2375.

Comparison of Build Methods

Method Daemon Required Privileged Mode Use Case
Docker-in-Docker (DinD) Yes Yes Standard GitLab CI pipelines with Docker executor
Kaniko No No High-security environments, Kubernetes-based runners
Podman No No Daemonless requirements, alternative to Docker
Shell Executor Yes (on host) No Legacy Windows servers or specific hardware requirements

Conclusion

The implementation of Docker builds within GitLab CI/CD is a sophisticated process that requires a deep understanding of container networking and daemon communication. While the standard Docker-in-Docker approach provides a flexible way to build and push images, it introduces dependencies on service aliases and specific environment variables like DOCKER_HOST to prevent connectivity failures. The shift toward daemonless tools like Kaniko and Podman reflects an industry-wide move toward greater security by eliminating the need for privileged containers. Furthermore, the disparity between Linux and Windows environments highlights the importance of selecting the correct OS version and runner executor to avoid the limitations inherent in older Windows Server releases. Ultimately, a robust pipeline integrates authentication via built-in CI variables, employs --pull strategies to ensure image freshness, and utilizes unique SHA tagging to maintain an immutable and traceable history of deployments.

Sources

  1. GitLab Documentation - Build and push container images
  2. PythonSpeed - Building Docker images on GitLab CI
  3. Snorre.io - Building Docker images with GitLab CI
  4. Docker Forums - Docker build in GitLab CI
  5. GitLab Forum - Using gitlab-ci.yml to create a Docker image

Related Posts