Skip to content

CI image builds

Build container images with plain docker build in docker-in-docker, pinned to the Incus runners with tags: [dind]. Those are the only runners where a privileged build is safe — their gitlab-runner sits inside an unprivileged Incus container, so the build is namespaced and cannot reach the host; the unprivileged bare-metal runners no longer carry the dind tag. Rationale: gitlab-runners.md.

build:image:
tags: [dind]
image: docker:28
services:
- docker:28-dind
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin
script:
- docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"

The runners set DOCKER_BUILDKIT=1, so BuildKit features work out of the box:

  • Secrets (private package index, etc.): docker build --secret id=TOKEN with RUN --mount=type=secret,id=TOKEN in the Dockerfile — credentials never land in the image.
  • Cache: --cache-from "$IMAGE" --build-arg BUILDKIT_INLINE_CACHE=1.
  • Multi-stage target: --target STAGE.

Don’t reach for kaniko or a standalone buildctl job — plain docker build on the dind runners covers it.

The same applies to any job that needs a live Docker daemon — a docker compose stack for end-to-end tests, for example: give it tags: [dind] so it lands on the Incus runners. A job that uses services: docker:dind without the tag may be scheduled onto an unprivileged runner where the dind service cannot start.