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_CERTDIRto enable TLS between the client and the dind service and mount the certs via the runnervolumessetting. - 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 --pushinstead ofbuildx.
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.