Setting up GitLab Runner to build Docker images

By Ward Pieters on

Intro

This post describes the process I went through on how to run a self‑hosted GitLab Runner with the Docker executor and Docker‑in‑Docker to build and push container images.

Below you'll find the relevant parts of my GitLab Runner configuration, and an example .gitlab-ci.yml that builds and pushes images to the registry.

Prerequisites

  • A self-hosted GitLab instance.
  • A virtual machine or server to run the GitLab Runner.
  • Docker installed on the GitLab Runner host.
  • Basic knowledge of GitLab CI/CD and Docker.

Setting up a GitLab Runner instance

To set up a GitLab Runner instance that can build Docker images, use below Docker compose file as a starting point. Adjust the runner token, image, environment variables and other settings to your needs.

services:
  gitlab-runner:
    image: gitlab/gitlab-runner:latest
    container_name: gitlab-runner
    restart: unless-stopped
    volumes:
      - ./config:/etc/gitlab-runner
      - /var/run/docker.sock:/var/run/docker.sock

Configuring the GitLab Runner

In the config directory, you'll need to create or edit the config.toml file to configure the runner. Below is an example configuration that enables Docker-in-Docker builds.

The key settings here are setting the executor to docker, enabling privileged mode, and adding the /certs/client volume for TLS certificates.

concurrent = 4
check_interval = 0
shutdown_timeout = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "gitlab-runner"
  url = "https://git.example.com"
  id = some-runner-id
  token = "some-runner-registration-token"
  token_obtained_at = 2025-11-03T12:30:25Z
  token_expires_at = 0001-01-01T00:00:00Z
  executor = "docker"
  request_concurrency = 4

  [runners.cache]
    MaxUploadedArchiveSize = 0
    [runners.cache.s3]
    [runners.cache.gcs]
    [runners.cache.azure]
  [runners.docker]
    tls_verify = false
    image = "node:latest"
    privileged = true
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false

    # https://docs.gitlab.com/ci/docker/using_docker_build#docker-in-docker-with-tls-enabled-in-the-docker-executor
    volumes = ["/certs/client", "/cache"]
    shm_size = 0
    network_mtu = 0

Warning

Setting privileged to true effectively disables the container’s security mechanisms and exposes your host to privilege escalation. This action can cause container breakout. For more information, see runtime privilege and Linux capabilities.

Example CI/CD configuration

This CI configiration uses docker:28.5 as the image, and docker:28.5-dind as a service. It logs in to the registry, builds the image using buildx, and pushes the image. Adjust tags, cache and build options to taste.

default:
  image: docker:28.5

variables:
  DOCKER_TLS_CERTDIR: "/certs"

.before-docker:
  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

stages:
  - build
  - publish

Build image:
  stage: build
  services:
    - docker:28.5-dind
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  before_script:
    - !reference [.before-docker]
  script:
    - >
      docker buildx build
      --push
      --provenance=false
      --cache-from $CI_REGISTRY_IMAGE:latest
      -f Dockerfile
      -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
      .

Push latest:
  stage: publish
  services:
    - docker:28.5-dind
  variables:
    GIT_STRATEGY: none
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  before_script:
    - !reference [.before-docker]
  script:
    - docker manifest create $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - docker manifest push $CI_REGISTRY_IMAGE:latest

Tips and troubleshooting

  • If jobs fail to connect to the dind service, make sure your GitLab Runner is registered and the runner's Docker daemon supports the dind setup (privileged mode, matching docker versions).
  • Use DOCKER_TLS_CERTDIR to enable TLS between the client and the dind service and mount the certs via the runner volumes setting.
  • For faster builds, use build cache (--cache-from) or set up a dedicated cache registry.
  • If you don't need multi-platform buildx features, you can simplify to docker build --push instead of buildx.

Problems?

If you find mistakes or have suggestions, let me know. If you encounter problems, check the runner registration, docker versions and privileged settings first.