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 = true in the runner’s config.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 with rules: - 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.

  • GitLab
  • Docker
  • CI/CD