GitLab includes a container registry with every project, and GitLab CI can build images on each push and store them there. The tricky part is how to run docker build inside a CI job, which itself runs in a container. Here are the common approaches, with working .gitlab-ci.yml examples.
Runner setup: GitLab Runner. Self-hosted GitLab: install GitLab on Ubuntu.
Option 1: Docker-in-Docker (dind)
The job runs the docker CLI, talking to a separate docker:dind service container:
# .gitlab-ci.yml
stages: [build, deploy]
variables:
DOCKER_TLS_CERTDIR: "/certs"
IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
build:
stage: build
image: docker:27
services:
- docker:27-dind
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
script:
- docker pull $CI_REGISTRY_IMAGE:latest || true
- docker build --cache-from $CI_REGISTRY_IMAGE:latest -t $IMAGE -t $CI_REGISTRY_IMAGE:latest .
- docker push $IMAGE
- docker push $CI_REGISTRY_IMAGE:latest
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
CI_REGISTRY*variables are provided automatically, so there’s no manual token for the project’s own registry.- Requires a runner with the Docker executor in privileged mode (
privileged = truein the runner’sconfig.toml). Only use that on runners you control, for trusted projects.
Option 2: bind the host’s Docker socket
Mount /var/run/docker.sock into job containers (runner config volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]). It’s fast, and the layer cache persists on the host. But any job can control the host’s Docker daemon, which is effectively root on the runner host. Acceptable only for a dedicated runner VPS used by trusted projects.
Option 3: rootless builds with Kaniko or Buildah
These build images without a Docker daemon and without privileged mode, which suits shared runners and Kubernetes executors.
Kaniko:
build:
stage: build
image:
name: gcr.io/kaniko-project/executor:v1.23.2-debug
entrypoint: [""]
script:
- /kaniko/executor
--context "$CI_PROJECT_DIR"
--dockerfile "$CI_PROJECT_DIR/Dockerfile"
--destination "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
--cache=true
Kaniko reads registry credentials from /kaniko/.docker/config.json. Recent GitLab versions handle authentication to the project registry automatically for Kaniko jobs in many setups, otherwise write the file from CI_REGISTRY_* variables. Note that Kaniko’s original project was archived by Google, and community forks continue it. Check for a maintained image.
Buildah:
build:
image: quay.io/buildah/stable
script:
- buildah login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
- buildah bud -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
- buildah push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
Tagging
- Commit SHA for every build (
$CI_COMMIT_SHORT_SHA). - Version tags for releases (
$CI_COMMIT_TAG, in a job withrules: - if: $CI_COMMIT_TAG). - Avoid deploying
latest. See Docker image tags best practices.
Deploy after building
deploy:
stage: deploy
image: alpine:3.20
environment: production
before_script:
- apk add --no-cache openssh-client
- install -m 600 "$SSH_PRIVATE_KEY" ~/.ssh_key # File-type CI/CD variable
script:
- ssh -i ~/.ssh_key -o StrictHostKeyChecking=accept-new deploy@$VPS_HOST "cd /opt/app && sed -i 's/^APP_IMAGE_TAG=.*/APP_IMAGE_TAG=$CI_COMMIT_SHORT_SHA/' .env && docker compose pull && docker compose up -d"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
The VPS must log in to the GitLab registry once (docker login registry.gitlab.example.com with a deploy token with read_registry). See GitLab CI deploy over SSH.
Scanning
GitLab includes container scanning templates (Ultimate tier), or run Trivy in a job on any tier. See Trivy image scanning.
Quick reference
| Method | Needs | Security |
|---|---|---|
| Docker-in-Docker | Privileged runner | Moderate (isolated daemon per job) |
| Socket binding | Docker executor + socket mount | Low (host root equivalent) |
| Kaniko / Buildah | Any runner, no privileges | Best for shared runners |
| Registry login | $CI_REGISTRY_USER / $CI_REGISTRY_PASSWORD |
Automatic job token |
Related: GitHub Actions Docker builds and Dockerfile tutorial.