Orchestrating Containerized Workflows: Technical Architectures for Docker Build in GitLab CI

The integration of containerization within Continuous Integration and Continuous Deployment (CI/CD) pipelines represents a fundamental pillar of modern DevOps. When utilizing GitLab CI/CD, the objective is frequently to automate the lifecycle of an application—from source code commit to the creation of a deployable container image. However, implementing a docker build command within a GitLab CI environment introduces a specific architectural paradox: GitLab CI jobs are designed to run inside Docker containers by default, yet the process of building a Docker image requires the ability to execute Docker commands, which necessitates a Docker daemon (dockerd). This requirement creates a "nested" execution environment where a container must manage the lifecycle of other containers. Navigating this requirement requires a deep understanding of executor types, daemon management, and alternative container runtimes to ensure security, speed, and reliability in the software delivery pipeline.

The Architectural Challenge of Nested Containerization

The primary obstacle in GitLab CI/CD is the inherent nature of the job execution environment. When a runner picks up a job, it typically spawns a container to host the job's shell and execution context. If the job's script contains a command such as docker build, the client-side Docker CLI inside that container will attempt to communicate with a Docker daemon. In a standard containerized environment, no such daemon is present; the container is isolated.

To overcome this, engineers must implement a strategy that provides the job with access to a Docker daemon. There are three primary architectural paths to solve this: leveraging Docker-in-Docker (DinD), utilizing the Shell executor, or adopting daemonless container tools like Podman. The choice between these impacts the security posture of the runner, the complexity of the .gitlab-ci.yml configuration, and the ability to utilize privileged modes.

Implementing Docker-in-Docker (DinD)

Docker-in-Docker (DinD) is the traditional and most common method for executing Docker commands within a GitLab CI job. This method involves running a secondary container that acts as the Docker daemon, providing the necessary services for the primary job container to perform image builds, pulls, and pushes.

Mechanism of Action

In a DinD configuration, the GitLab CI job is configured to run a service alongside the main job image. The services keyword in the .gitlab-ci.yml file is used to launch the docker:dind image. This service container runs the dockerd daemon. To enable communication between the job container (the client) and the service container (the daemon), the DOCKER_HOST environment variable must be explicitly defined. This variable tells the Docker CLI that the daemon is not local to the job container but is reachable via a network socket, typically through the service's alias.

Configuration and Variables

To successfully implement DinD, the following variables and configurations are essential:

  • DOCKER_HOST: This environment variable specifies the connection endpoint. For example, tcp://dockerdaemon:2375/ tells the CLI to look for the daemon at the hostname provided by the service alias.
  • services: This section specifies the docker:dind image. An alias is often provided to ensure the DOCKER_HOST can resolve the service name to an IP address within the container network.
  • image: The job itself requires a Docker-enabled image, such as docker:latest or a specific version like docker:24.0.5-cli.

Practical Configuration Example

The following configuration demonstrates a job that uses a specific version of the Docker CLI and the DinD service to build and test an image.

yaml build: image: $CI_REGISTRY/group/project/docker:24.0.5-cli services: - name: $CI_REGISTRY/group/project/docker:24.0.5-dind alias: docker stage: build variables: DOCKER_HOST: tcp://docker:2375/ script: - docker build -t my-docker-image . - docker run my-docker-image /script/to/run/tests

Security and Privileged Mode

A critical requirement for the DinD approach is that the GitLab Runner must be configured to support privileged mode. Because DinD essentially runs a daemon inside a container, it requires elevated permissions to manage network interfaces, filesystems, and container lifecycles. Enabling privileged mode on a runner can introduce security risks, as a compromised container could potentially gain control over the host machine. For organizations with strict security requirements, this is a significant consideration.

Alternative Execution Strategies

Given the security implications of privileged DinD, several alternative strategies exist to achieve container builds.

The Shell Executor

The Shell executor is the simplest method to enable Docker commands. In this configuration, the GitLab Runner is installed directly on a host machine (such as a virtual machine or a physical server) rather than running as a container. The gitlab-runner user on the host is granted permission to execute Docker commands.

When using the shell executor, the docker command is executed directly on the host's operating system. This avoids the "container-within-a-container" problem entirely. However, it requires the Docker Engine to be installed on the host machine where the runner resides.

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

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

The impact of using the shell executor is a reduction in isolation. Since the commands run on the host, there is a higher risk of side effects on the host machine if the CI scripts are not carefully managed.

Podman: The Daemonless Alternative

Podman offers a fundamentally different architecture compared to Docker. While Docker relies on a centralized daemon (dockerd) to manage all container operations, Podman is daemonless. The Podman CLI performs the work itself, interacting directly with the container runtime.

This architecture is highly advantageous for GitLab CI/CD because it allows for container builds without requiring privileged mode or a running daemon service. This simplifies the .gitlab_ci.yml file and enhances security.

Implementing Podman in GitLab CI

Using Podman is remarkably straightforward because it supports the same command-line options as Docker. A developer can switch from Docker to Podman by simply changing the image and the commands used in the script section.

Direct Podman Usage

In this example, the job uses a Podman-specific image to build and push to the GitLab Container Registry.

```yaml
stages:
- build

podman-build:
stage: build
image:
name: quay.io/podman/stable
script:
- podman login -u "$CIREGISTRYUSER" -p "$CIREGISTRYPASSWORD" "$CIREGISTRY"
- podman build -t "$CI
REGISTRYIMAGE:podman" .
- podman push "$CI
REGISTRY_IMAGE:podman"
```

Emulating Docker with Podman

For teams that prefer to keep their existing Docker-based scripts, Podman provides a compatibility layer. By creating a symbolic link between the podman binary and a docker binary within the container, the existing scripts can function without modification.

```yaml
stages:
- build

podman-build:
stage: build
image:
name: quay.io/podman/stable
script:
- ln -s /usr/bin/podman /usr/bin/docker
- docker login -u "$CIREGISTRYUSER" -p "$CIREGISTRYPASSWORD" "$CIREGISTRY"
- docker build -t "$CI
REGISTRYIMAGE:podman" .
- docker push "$CI
REGISTRY_IMAGE:podman"
```

Advanced Optimization: Multistage Dockerfiles

A significant advancement in containerized builds is the use of multi-stage Dockerfiles. This technique allows developers to separate the build environment from the final production environment within a single Dockerfile. This process effectively reduces the complexity of GitLab CI pipelines by moving the build logic out of the .gitlab-ci.yml and into the Dockerfile itself.

The Mechanics of Multi-stage Builds

In a multi-stage build, a single Dockerfile contains multiple FROM instructions. Each FROM instruction starts a new stage of the build. In the first stage (often called the builder stage), all the necessary tools, compilers, and dependencies required to build the application are installed. Once the build is complete, the second stage starts from a much smaller, lightweight base image (such as alpine). The developer then uses the COPY --from command to pull only the necessary artifacts (e.g., the compiled binary or the dist folder) from the builder stage into the final production stage.

Comparison of Build Strategies

Feature Docker-in-Docker (DinD) Shell Executor Podman
Daemon Required Yes (dockerd) Yes (on Host) No (Daemonless)
Privileged Mode Required Not applicable Not required
Complexity High (requires services/variables) Low Low
Security Isolation Lower (due to privileged mode) Lowest (runs on host) Highest

Example: Multi-stage Node.js Workflow

The following Dockerfile demonstrates how to leverage multi-stage builds to create a highly optimized, small-footprint image for a Node.js application.

```dockerfile

Stage 1: The Builder

FROM node:8 as builder
WORKDIR /usr/src/app
COPY package.json .
RUN npm install
COPY ./src /usr/src/app/
RUN npm run build

Stage 2: The Production Image

FROM node:8-alpine
WORKDIR /usr/src/app
COPY package.json .
RUN npm install --production
COPY --from=builder /usr/src/app/dist /usr/src/app/dist
COPY --from=builder /usr/src/app/server.js /usr/src/app/server.js
CMD ["node", "server.js"]
```

The impact of this approach is a significant reduction in the final image size and a reduced attack surface, as the final image does not contain the build-time dependencies like npm or the source code files used during compilation.

Registry Management and Image Tagging

Efficiently managing images within the GitLab Container Registry is vital for pipeline reliability and speed.

Tagging Strategies

When pushing images to the registry, the choice of tags determines how images are identified and reused.

  • Git SHA Tagging: Using the Git commit SHA (e.g., $CI_COMMIT_SHA) as the image tag ensures that every single job produces a unique, immutable image. This is highly recommended for production environments to avoid "stale" images.
  • The latest Tag: It is strongly advised to avoid building directly to the latest tag. In high-concurrency environments, multiple jobs might attempt to push to the latest tag simultaneously, leading to race conditions and unpredictable deployment states.
  • Dependency Risks: Even when using unique Git SHA tags, a rebuild of a specific commit might result in a different image if external dependencies (e.g., via npm install) have changed.

The Dependency Proxy

To accelerate builds and avoid hitting rate limits imposed by external registries like Docker Hub, GitLab provides the Dependency Proxy. By using the Dependency Proxy prefix in the .gitlab-ci.yml file, the runner pulls images through GitLab's own cache, which improves performance and ensures smoother CI/CD execution.

Conclusion

Architecting a robust docker build process in GitLab CI/CD requires a strategic selection between DinD, the Shell executor, and Podman, based on the specific security and operational constraints of the environment. While Docker-in-Docker remains a standard, its reliance on privileged mode makes Podman an increasingly attractive, secure, and simplified alternative. Furthermore, the transition from complex CI-managed build stages to streamlined multi-stage Dockerfiles represents a shift toward "container-native" development, where the build logic is encapsulated within the image definition itself. By combining these advanced techniques—utilizing Podman for daemonless builds, implementing multi-stage Dockerfiles for optimization, and employing strict Git SHA tagging for immutability—engineers can build highly scalable, secure, and efficient containerized deployment pipelines.

Sources

  1. PythonSpeed: Building Docker images on GitLab CI
  2. GitLab Documentation: Using Docker in GitLab CI/CD
  3. Snorre.io: Building Docker images with GitLab CI
  4. GitLab Documentation: Build and push images to Container Registry

Related Posts