Orchestrating Containerized Workflows via GitLab CI/CD and Docker

The convergence of containerization and continuous integration represents a cornerstone of modern DevOps engineering. When developers seek to automate the lifecycle of an application—from source code commitment to the deployment of a production-ready container—GitLab CI/CD provides a robust framework for executing docker build operations. This orchestration is not merely a matter of executing a single command; it involves a complex interplay between runners, daemons, authentication protocols, and the architectural choice between various container engines. To achieve a seamless pipeline, one must navigate the intricacies of Docker-in-Docker (DinD) architectures, the daemon-less advantages of Podman, the security implications of privileged modes, and the efficiency gains provided by multi-stage Dockerfiles. Understanding these layers is essential for any engineer tasked with building, testing, and pushing images to a container registry within a GitLab environment, whether utilizing GitLab.com, GitLab Self-Managed, or GitLab Dedicated instances.

Architectural Paradigms for Containerized CI/CD

The fundamental challenge in building Docker images within a GitLab CI/CD pipeline arises from the nature of the GitLab Runner itself. By default, GitLab CI jobs are designed to run inside isolated Docker containers. This creates a recursive architectural problem: if a job is already encapsulated within a container, how does it command another container engine to build an image? This "container-within-a-container" requirement necessitates specific configuration strategies to ensure the build environment has the necessary tools and permissions to interact with a container daemon.

The available tiers for these services—Free, Premium, and Ultimate—apply across all major GitLab offerings, including GitLab.com, GitLab Self-Managed, and GitLab Dedicated. Regardless of the tier, the underlying technical hurdle remains the same: providing the job environment with the ability to execute Docker commands effectively.

The Docker-in-Docker (DinD) Approach

The most traditional method for overcoming the container isolation barrier is the Docker-in-Docker (DinD) technique. In a standard environment, the Docker Command Line Interface (CLI) acts as a client that communicates with the dockerd daemon. The dockerd daemon is the server component responsible for the heavy lifting: managing images, containers, networks, and volumes. In a GitLab CI environment, the dockerd daemon is not natively available to the job container.

To solve this, GitLab CI allows for the execution of "services." A service is essentially a secondary container that runs alongside the primary job container. By utilizing the docker:dind image as a service, a separate daemon is spun up to handle the containerization tasks.

To implement this, the .gitlab-ci.yml file must be configured to define the service and establish a communication bridge between the client and the daemon. The following configuration demonstrates how to set up a dind-build job:

yaml dind-build: services: - name: docker:dind alias: dockerdaemon variables: DOCKER_HOST: tcp://dockerdaemon:2375/ script: - docker build -t my-image .

In this configuration, the alias provides a predictable hostname (dockerdaemon) that the CLI can reach. The DOCKER_HOST environment variable is critical; it instructs the Docker CLI to redirect its requests from a local Unix socket to the network socket of the docker:dind service. This method, however, carries significant security implications. To function correctly, the GitLab Runner must be configured to support Docker commands, which typically requires enabling "privileged mode." Running in privileged mode grants the container extended permissions on the host machine, which can increase the attack surface of the infrastructure.

The Podman Alternative

For organizations seeking to avoid the security risks associated with privileged mode, Podman offers a compelling alternative. Unlike Docker, Podman utilizes a fundamentally different architecture. It is a daemon-less container engine, meaning there is no persistent background process like dockerd that requires elevated host permissions to manage. The Podman CLI performs the work itself, making it highly compatible with unprivileged environments.

Because Podman is designed to be a drop-in replacement for Docker, it supports almost all the same command-line options. This allows for a significantly simpler GitLab CI configuration that does not require the overhead of a secondary service or the risks of privileged execution.

There are two primary ways to implement Podman in a GitLab pipeline:

  1. Direct Podman usage:
    The job uses a Podman-based image and executes commands directly.

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

  1. Simulated Docker usage:
    For teams with existing scripts heavily reliant on the docker command, Podman can be "masked" as Docker by creating a symbolic link. This allows the use of the docker syntax while the underlying engine is actually Podman.

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

The Shell Executor Method

An alternative to the container-based approaches is the use of the Shell executor. In this configuration, the GitLab Runner does not run the job inside a container; instead, it executes the commands directly on the host machine where the runner is installed.

To utilize this method, the following prerequisites must be met:

  • The GitLab Runner must be installed on the target server.
  • The Runner must be registered with the Shell executor selected during the registration process.
  • The Docker Engine must be installed on the same server as the GitLab Runner.
  • The gitlab-runner user must have the necessary permissions to execute Docker commands on the host.

The registration command for a shell executor follows this structure:

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

While the Shell executor simplifies the "container-in-container" problem, it requires careful management of the host environment, as the job has direct access to the host's shell and resources.

Container Registry Integration and Image Management

Once the build process is established, the final stage of the containerized pipeline is the movement of the resulting image to a registry. GitLab provides a built-in Container Registry that integrates seamlessly with its CI/CD features.

Authentication and Image Lifecycle

Before any image can be pushed to the registry, the runner must authenticate. In GitLab CI/CD, this is typically handled using predefined environment variables that represent the current user and registry credentials.

The standard workflow for building and pushing an image involves three primary steps:

  • Authentication: Logging into the registry using the provided credentials.
  • Building: Executing the build command with an appropriate tag.
  • Pushing: Uploading the built image to the remote repository.
Command Type Syntax Example Purpose
Build docker build -t registry.example.com/group/project/image . Creates the image from a Dockerfile in the current directory.
Push docker push registry.example.com/group/project/image Uploads the local image to the remote registry.

To optimize the build process and ensure reliability, certain best practices should be followed:

  • Use docker build --pull: This flag forces the engine to check for newer versions of the base images specified in the Dockerfile. While this adds a small amount of time to the build, it prevents the use of stale, potentially insecure base layers.
  • Explicit docker pull: When using multiple runners, images may not be cached locally on every node. Performing an explicit docker pull before a docker run command ensures that the specific image version just built is actually available to the container being launched.
  • Unique Tagging: Utilizing the Git SHA (commit hash) in the image tag (e.g., image:$CI_COMMIT_SHA) ensures that every job produces a unique, identifiable image. This eliminates the risk of "stale" images where a job might accidentally use a previous version of the code.

Efficient Image Construction with Multi-stage Builds

A significant advancement in Docker development is the implementation of multi-stage builds. This feature allows developers to consolidate the entire build-and-package lifecycle into a single Dockerfile, drastically reducing the complexity of the .gitlab-ci.yml file. Previously, complex pipelines required separate GitLab CI stages to compile code and then another stage to wrap that code into an image. With multi-stage builds, the "build" and "production" logic are handled internally by the Docker engine.

The mechanism relies on the FROM ... AS ... syntax. The first stage (the "builder" stage) contains all the heavy tools required for compilation (compilers, build tools, development libraries). Once the build is complete, the second stage starts from a much smaller, leaner base image. The COPY --from instruction is then used to pluck only the necessary artifacts from the builder stage and place them into the final, production-ready image.

The following example illustrates a multi-stage Dockerfile for a Node.js application:

```dockerfile

Stage 1: The builder stage

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 stage

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

In this workflow, the node:8-alpine image used in the second stage provides a minimal footprint, reducing the attack surface and speeding up deployment. The final image does not contain the npm build tools or the source files used during the build process; it only contains the runtime dependencies and the compiled artifacts.

Technical Comparison of GitLab CI Build Strategies

Choosing the correct method for building images requires a trade-off between security, simplicity, and performance. The following table provides a high-level comparison of the three primary strategies discussed.

Feature Docker-in-Docker (DinD) Podman Shell Executor
Security Level Low (Requires Privileged Mode) High (Daemon-less/Unprivileged) Variable (Depends on Host)
Complexity High (Requires Service/DOCKER_HOST) Low (Direct CLI usage) Low (Direct Host execution)
Isolation High (Container-based) High (Container-based) Low (Host-based)
Best Use Case Standard Docker workflows Security-sensitive environments Single-server/Legacy setups

Conclusion

The evolution of docker build within GitLab CI/CD reflects the broader maturation of the DevOps ecosystem. What began as a complex workaround involving privileged containers and manual orchestration has evolved into a streamlined experience through the adoption of Podman's daemon-less architecture and the elegance of multi-stage Dockerfiles. Engineers must weigh the necessity of isolation against the requirements of security; while DinD provides a familiar environment, Podman offers a path toward more secure, unprivileged pipelines. Furthermore, the transition from multi-stage GitLab CI pipelines to single-stage multi-stage Dockerfiles represents a fundamental shift toward "single source of truth" configuration, where the container's structure is defined by the container engine itself rather than the CI orchestration layer. Mastering these nuances allows for the construction of highly efficient, scalable, and secure continuous integration workflows.

Sources

  1. GitLab Documentation: Use Docker to build Docker images
  2. PythonSpeed: Building Docker images on GitLab CI
  3. GitLab Documentation: Build and push container images to the container registry
  4. Snorre.io: Building Docker images with GitLab CI

Related Posts