Example GitLab Runner docker compose configuration

After having spent some time the other day to redo my (“standalone”) runner setup, I figured that I would share this for the benefit of the community.

.env:

RUNNER_NAME=RUNNER-NAME
REGISTRATION_TOKEN=TOKEN
CI_SERVER_URL=https://gitlab.com/

docker-compose.yml:

version: "3.5"

services:
  dind:
    image: docker:20-dind
    restart: always
    privileged: true
    environment:
      DOCKER_TLS_CERTDIR: ""
    command:
      - --storage-driver=overlay2

  runner:
    restart: always
    image: registry.gitlab.com/gitlab-org/gitlab-runner:alpine
    depends_on:
      - dind
    environment:
      - DOCKER_HOST=tcp://dind:2375
    volumes:
      - ./config:/etc/gitlab-runner:z

  register-runner:
    restart: 'no'
    image: registry.gitlab.com/gitlab-org/gitlab-runner:alpine
    depends_on:
      - dind
    environment:
      - CI_SERVER_URL=${CI_SERVER_URL}
      - REGISTRATION_TOKEN=${REGISTRATION_TOKEN}
    command:
      - register
      - --non-interactive
      - --locked=false
      - --name=${RUNNER_NAME}
      - --executor=docker
      - --docker-image=docker:20-dind
      - --docker-volumes=/var/run/docker.sock:/var/run/docker.sock
    volumes:
      - ./config:/etc/gitlab-runner:z

Source: TyIsI / gitlab-runner-docker-compose · GitLab

Notes:

  • Because networks are not specified, docker-compose will create project network, which defaults to bridge mode.
  • The docker references can be a little bit confusing. So let me try to clear that up:
    • By setting DOCKER_TLS_CERTDIR to empty, the dind instance is forced to use plain TCP
    • The runner connects to dind over TCP.
    • The docker.sock referenced in the register-runner is in reference to the dind executor. It’s how the containers in the docker container can talk to docker.
  • This config runs fully in dind.
  • This config is based on a config that I found and then optimized (and brought up to docker 20).
  • The config in the repo does some filesystem caching.

Hope this helps anyone!

6 Likes

I was struggling for hours in order to have a full gitlab ci/cd-> docker-compose runner configuration up and running with no luck till now.
At a certain point it was clear to me that i needed to configure a dind service for my docker runner but i was unable to make it work correctly.
Now i ran your docker-compose and its working like a charm!
Thanks man!

1 Like

Great work, @ TyIsI
However, I’m not able to start the dind container:

ip: can't find device 'ip_tables'
ip_tables              36864  0 
x_tables               53248 12 xt_state,xt_ipvs,xt_nat,xt_policy,xt_mark,xt_u32,xt_tcpudp,xt_conntrack,xt_MASQUERADE,xt_addrtype,nft_compat,ip_tables
modprobe: can't change directory to '/lib/modules': No such file or directory
mount: permission denied (are you root?)
Could not mount /sys/kernel/security.
AppArmor detection and --privileged mode might break.
mount: permission denied (are you root?)

Any clues ?

@aguinaldoabbj

Sorry for the late reply!

I’m getting the same error, but it continues fine after:

What do you see in the dind logging? (docker-compose logs -f dind)

The errors you see are because of the way that Docker normally works. (It sets up NAT with IP tables to allow incoming traffic to containers. However, as this is not required for dind, it’s safe to ignore this.)

Hi @TyIsI ,

Log shows the same as yours.

However, the “dind” service keeps dying and the register-runner repeats indefinitely starting/registering/dying cycles. The limit of 50 registered runners is reached, but no runner gets online.

Still, any clues?

hello

i didnot get the gitlab-runner to run in the qnap container. I get the following error message:

Runtime platform arch=amd64 os=linux pid=7 revision=456e3482 version=15.10.0
Starting multi-runner from /etc/gitlab-runner/config.toml… builds=0
Running in system-mode.

WARNING: There might be a problem with your config
jsonschema: ‘/runners’ does not validate with https://gitlab.com/gitlab-org/gitlab-runner/common/config#/$ref/properties/runners/type: expected array, but got null
Created missing unique system ID system_id=r_OqExoOxwLG52
Configuration loaded builds=0
listen_address not defined, metrics & debug endpoints disabled builds=0
[session_server].listen_address not defined, session endpoints disabled builds=0
Initializing executor providers builds=0
ERROR: Failed to load config stat /etc/gitlab-runner/config.toml: no such file or directory builds=0

Your config runs perfectly right out the box, thank you for save my time a lot, man.

I kept getting this error:
ERROR: error during connect: Head "http://docker:2375/_ping": dial tcp: lookup docker on 172.31.0.2:53: no such host

Here’s what I did to solve it:
I added this block on the config.toml:

environment = [
  "DOCKER_HOST=tcp://docker:2376",  "DOCKER_TLS_CERTDIR=/certs"
]

So the whole file looks like this:

[[runners]]
  name = "runner-name"
  url = "https://gitlab.com/"
  id = 1234567
  token = "<TOKEN-HERE>"
  token_obtained_at = 2025-01-18T04:08:16Z
  token_expires_at = 0001-01-01T00:00:00Z
  executor = "docker"
  [runners.custom_build_dir]
  [runners.cache]
    MaxUploadedArchiveSize = 0
    [runners.cache.s3]
    [runners.cache.gcs]
    [runners.cache.azure]
  [runners.docker]
    tls_verify = false
    image = "docker:20-dind"
    privileged = false
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
    shm_size = 0
    network_mtu = 0
    environment = ["DOCKER_HOST=tcp://docker:2376","DOCKER_TLS_CERTDIR=/certs"    ]

Also, consider stopping the register-runner in docker-compose file so a new one is not created everytime you run docker compose up command.

Hope this helps someone

just tried this and I’m encountering a few issues:

  1. How do I inject the proper DNS server using the docker-compose.yml ? for some reason our DNS is not working inside the runner container
error "fatal: unable to access '/associates/myProject.git/': Could not resolve host: mygitlab.host.com"
  1. At work we have a MITM proxy, which means every docker container must spin up with our rootCA added to the update-ca-certificates (cp /etc/gitlab-runner/ca-certificates.crt /usr/local/share/ca-certificates/ && update-ca-certificates) the proper rootCA (ca-certificates.crt) is in the config subfolder when doing docker compose up -d
error " **x509: certificate signed by unknown authority**"
  1. “No such image” error due to reaching the maximum pulls, and not having done a docker login. Should I add a docker login to the docker-compose.yml ??
ERROR: Preparation failed: Error: No such image:
  1. Would be nice if the docker-compose also did the gitlab-runner unregister --name “runner-name” , each time we compose down

so according to this , it is necessary to specify helper_image: in the config.toml to override the default helper_image that doesn’t seem to work. Can it really be this complicated?




I have confirmed this works by execing into the container and adding that value to the runners.docker in the config.toml. So how do I edit the docker-compose.yml to inject this into the config.toml ?

Hello,

Here’s my recommendation for enhancing your GitLab Runner setup with the latest best practices:

Key Improvements

  1. Latest Images: Using latest stable releases of Docker and GitLab Runner for security and features
  2. Enforced TLS: Fully secured with TLS certificates for all Docker communications
  3. Intelligent Auto-Registration: Runner only registers when needed, preventing duplicates
  4. Health Checks: DinD health check ensures the Docker engine is fully operational before runner connects (see advance-docker-compose)
  5. Default Parameters: Added sensible defaults for environment variables with override capability
  6. Volume Management: Dedicated Docker volume for better performance and data persistence

This setup offers the optimal balance of security, performance, and maintainability. The TLS implementation protects against unauthorized access to your Docker daemon, while the conditional registration script efficiently manages runner configuration.

Let me know if you need any adjustments to match your specific environment requirements.

Regards,

.env

RUNNER_NAME=RUNNER-NAME
REGISTRATION_TOKEN=TOKEN
CI_SERVER_URL=https://gitlab.com/

docker-compose.yml

services:
  dind:
    image: docker:dind
    container_name: dind
    restart: always
    privileged: true
    environment:
      DOCKER_TLS_CERTDIR: /certs
      DOCKER_TLS_SAN: DNS:dind
    command:
      - --storage-driver=overlay2
    volumes:
      - /data/docker/gitlab-runner/certs:/certs

  register-runner:
    container_name: register-runner
    image: gitlab/gitlab-runner:alpine3.18-bleeding
    restart: 'no'
    depends_on:
      - dind
    environment:
      - CI_SERVER_URL=${CI_SERVER_URL}
      - REGISTRATION_TOKEN=${REGISTRATION_TOKEN}
      - DOCKER_TLS_VERIFY=1
      - DOCKER_CERT_PATH=/certs/client
    entrypoint: ["/bin/sh", "-c"]
    # Check if config exists before registering
    command:
      - |
        if [ ! -f /etc/gitlab-runner/config.toml ]; then
          echo "Config does not exist, registering runner..."
          gitlab-runner register \
            --non-interactive \
            --locked=false \
            --name=${RUNNER_NAME} \
            --executor=docker \
            --docker-image=alpine:3.19 \
            --docker-host=tcp://dind:2376
        else
          echo "Runner config already exists, skipping registration"
        fi
    volumes:
      - /data/docker/gitlab-runner/config:/etc/gitlab-runner:z
      - /data/docker/gitlab-runner/certs:/certs:ro

  runner:
    container_name: gitlab-runner
    restart: always
    image: gitlab/gitlab-runner:alpine3.18-bleeding
    depends_on:
      - dind
    environment:
      - DOCKER_HOST=tcp://dind:2376
      - DOCKER_TLS_VERIFY=1
      - DOCKER_CERT_PATH=/certs/client
    volumes:
      - /data/docker/gitlab-runner/config:/etc/gitlab-runner:z
      - /data/docker/gitlab-runner/certs:/certs:ro

advance-docker-compose.yml

services:
  dind:
    image: docker:dind # Latest stable version
    restart: always
    privileged: true
    environment:
      DOCKER_TLS_CERTDIR: /certs
      DOCKER_TLS_SAN: DNS:dind
    volumes:
      - dind-storage:/var/lib/docker
      - /data/docker/gitlab-runner/certs:/certs
    healthcheck:
      test: ["CMD", "docker", "info"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 5s

  register-runner:
    image: gitlab/gitlab-runner:latest # Always use latest stable
    restart: 'no'
    depends_on:
      dind:
        condition: service_healthy
    entrypoint: ["/bin/sh", "-c"]
    command:
      - |
        if [ ! -f /etc/gitlab-runner/config.toml ]; then
          echo "Config does not exist, registering runner..."
          gitlab-runner register \
            --non-interactive \
            --locked=false \
            --name=${RUNNER_NAME:-"GitLab Runner"} \
            --executor=docker \
            --docker-image=alpine:latest \
            --docker-host=tcp://dind:2376 \
            --tag-list=${RUNNER_TAGS:-"docker,alpine"} \
            --run-untagged=${RUN_UNTAGGED:-true}
        else
          echo "Runner config already exists, skipping registration"
        fi
    environment:
      - CI_SERVER_URL=${CI_SERVER_URL}
      - REGISTRATION_TOKEN=${REGISTRATION_TOKEN}
      - DOCKER_TLS_VERIFY=1
      - DOCKER_CERT_PATH=/certs/client
    volumes:
      - /data/docker/gitlab-runner/config:/etc/gitlab-runner:z
      - /data/docker/gitlab-runner/certs:/certs:ro

  runner:
    image: gitlab/gitlab-runner:latest # Latest stable version
    restart: always
    depends_on:
      dind:
        condition: service_healthy
    environment:
      - DOCKER_HOST=tcp://dind:2376
      - DOCKER_TLS_VERIFY=1
      - DOCKER_CERT_PATH=/certs/client
    volumes:
      - /data/docker/gitlab-runner/config:/etc/gitlab-runner:z
      - /data/docker/gitlab-runner/certs:/certs:ro

volumes:
  dind-storage:

Enjoy

Thank you @mrioux for your scripts. I have set up my GitLab runner with your advance-docker-compose.yml example. The only change I have from your docker-compose.yml is I added --docker-network-mode=host to the end of the gitlab-runner register script so that it could find the DNS record for our self-hosted GitLab instance.

I’ve been trying to set up a new docker build job with the following in my .gitlab-ci.yml.

docker-build-job:
  stage: build
  image: docker:latest
  services:
    - name: docker:dind
      alias: dind
  variables:
    DOCKER_HOST: tcp://dind:2376
    DOCKER_TLS_CERTDIR: "/certs"
    DOCKER_CERT_PATH: "/certs/client"
    DOCKER_TLS_VERIFY: "1"
  script:
    - echo "Building Docker image..."
    - docker info
    #- docker build -t my-image:latest .

When the job runner runs this job, I get the following error final lines in my log output:

$ echo "Building Docker image..."
Building Docker image...
$ docker info
Failed to initialize: unable to resolve docker endpoint: open /certs/client/ca.pem: no such file or directory
Cleaning up project directory and file based variables
00:01
ERROR: Job failed: exit code 1

Also if I leave off the docker-build-job variables my log output ends:

$ echo "Building Docker image..."
Building Docker image...
$ docker info
Client:
 Version:    28.3.2
 Context:    default
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.26.1
    Path:     /usr/local/libexec/docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v2.39.1
    Path:     /usr/local/libexec/docker/cli-plugins/docker-compose
Server:
error during connect: Get "http://docker:2375/v1.51/info": dial tcp: lookup docker on 127.0.0.11:53: server misbehaving
Cleaning up project directory and file based variables
00:00
ERROR: Job failed: exit code 1

Do you have any insights on why the certs would not be available to the docker image in my docker-build-job and how to successfully connect back to the dind?

Couldn’t sleep tonight so continued looking into this. I now have a gitlab-runner register that works as intended.

 - |
        if [ ! -f /etc/gitlab-runner/config.toml ]; then
          echo "Config does not exist, registering runner..."
          gitlab-runner register \
            --non-interactive \
            --locked=false \
            --name=${RUNNER_NAME:-"GitLab Runner"} \
            --executor=docker \
            --docker-image=alpine:latest \
            --docker-host=tcp://dind:2376 \
            --tag-list=${RUNNER_TAGS:-"docker,alpine"} \
            --run-untagged=${RUN_UNTAGGED:-true} \
            --docker-network-mode=host \
            --docker-volumes=/certs/client:/certs/client:ro \
            --docker-volumes=/var/run/docker.sock:/var/run/docker.sock
        else
          echo "Runner config already exists, skipping registration"
        fi

I added the last 3 lines. Seems like the docker.sock was needed in my case.
My test build job is now:

docker-build-job:
  stage: build
  image: docker:latest
  before_script:
    - docker info
  script:
    - echo "Building Docker image..."

My full modified docker-compoose.yml

services:
  dind:
    image: docker:dind
    restart: always
    privileged: true
    environment:
      DOCKER_TLS_CERTDIR: /certs
      DOCKER_TLS_SAN: DNS:dind
    volumes:
      - dind-storage:/var/lib/docker
      - dind-certs:/certs
    healthcheck:
      test: ["CMD", "docker", "info"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 5s

  register-runner:
    image: gitlab/gitlab-runner:latest
    restart: 'no'
    depends_on:
      dind:
        condition: service_healthy
    entrypoint: ["/bin/sh", "-c"]
    command:
      - |
        if [ ! -f /etc/gitlab-runner/config.toml ]; then
          echo "Config does not exist, registering runner..."
          gitlab-runner register \
            --non-interactive \
            --locked=false \
            --name=${RUNNER_NAME:-"GitLab Runner"} \
            --executor=docker \
            --docker-image=alpine:latest \
            --docker-host=tcp://dind:2376 \
            --tag-list=${RUNNER_TAGS:-"docker,alpine"} \
            --run-untagged=${RUN_UNTAGGED:-true} \
            --docker-network-mode=host \
            --docker-volumes=/certs/client:/certs/client:ro \
            --docker-volumes=/var/run/docker.sock:/var/run/docker.sock
        else
          echo "Runner config already exists, skipping registration"
        fi
    environment:
      - CI_SERVER_URL=${CI_SERVER_URL}
      - REGISTRATION_TOKEN=${REGISTRATION_TOKEN}
      - DOCKER_TLS_VERIFY=1
      - DOCKER_CERT_PATH=/certs/client
    volumes:
      - runner-cfg:/etc/gitlab-runner:z
      - dind-certs:/certs:ro

  runner:
    image: gitlab/gitlab-runner:latest
    restart: always
    depends_on:
      dind:
        condition: service_healthy
    environment:
      - DOCKER_HOST=tcp://dind:2376
      - DOCKER_TLS_VERIFY=1
      - DOCKER_CERT_PATH=/certs/client
    volumes:
      - runner-cfg:/etc/gitlab-runner:z
      - dind-certs:/certs:ro

volumes:
  dind-storage:
  dind-certs:
  runner-cfg: