VERSICH

A practical guide to Building Docker image in Gitlab

a practical guide to building docker image in gitlab

Containerizing your application is only half the story; the real power comes when your Docker images are built automatically on every commit. GitLab CI makes this easy by letting you define a pipeline that builds and optionally pushes images to a container registry using just a .gitlab-ci.yml file. This guide walks through a simple, production‑style pattern for building Docker images in GitLab CI.

Prerequisites 

Before creating the pipeline, make sure you have: 

  • A GitLab project with your application and a Dockerfile at the root (or defined path). 

  • GitLab Container Registry is enabled for the project, or another registry you can push to. 

  • A GitLab Runner capable of running Docker‐in‐Docker (dind) or a Docker‑enabled executor.

You can check if Container Registry is enabled under Settings → Packages and registries → Container registry in your GitLab project.

Option 1: Build Docker Images with Docker‑in‑Docker 

The most common approach is to use Docker‑in‑Docker (dind): GitLab spins up a service container running the Docker daemon, and your job connects to it to run docker build commands. This pattern is well‑supported in GitLab Docs and works well for many teams.

A minimal .gitlab-ci.yml.yml using DIND looks like this: 

image: docker:25.0 

services: 
  - name: docker:25.0-dind 
    command: ["--tls=false"] 

variables: 
  DOCKER_TLS_CERTDIR: "" 
  DOCKER_DRIVER: overlay2 
  DOCKER_IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" 

stages: 
  - build 
  - push 

before_script: 
  - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" 

build-image: 
  stage: build 
  script: 
    - docker build -t "$DOCKER_IMAGE" . 
    - docker images 

push-image: 
  stage: push 
  script: 
    - docker push "$DOCKER_IMAGE" 
  only: 
    - main 

What this configuration does 

  • Uses the official Docker image for the job, and starts Docker:dind as a service so Docker commands work inside the CI job container.

  • Disables TLS for dind (DOCKER_TLS_CERTDIR: "" and --tls=false) to simplify configuration for basic setups.

  • Logs into the GitLab Container Registry using predefined CI variables (CI_REGISTRY_USER, CI_REGISTRY_PASSWORD, CI_REGISTRY).

  • Builds an image tagged with the project image plus the short commit SHA, then pushes it on the main branch.

This gives you a reproducible, traceable image per commit, ideal for staging or deployments.

Option 2: Build and Push with Kaniko or Similar Tools 

Docker‑in‑Docker works well, but some teams prefer rootless build tools like Kaniko or Podman to avoid running a Docker daemon in CI. These tools build images directly from the Dockerfile and push to a registry without requiring privileged Docker access.

A Kaniko‑style job in .gitlab-ci.yml could look like: 

stages: 
  - build 

build-kaniko: 
  stage: build 
  image: 
    name: gcr.io/kaniko-project/executor:latest 
    entrypoint: [""] 
  variables: 
    DOCKER_CONFIG: /kaniko/.docker/ 
    DESTINATION_IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" 
  script: 
    - mkdir -p /kaniko/.docker 
    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json 
    - /kaniko/executor 
        --context "$CI_PROJECT_DIR" 
        --dockerfile "$CI_PROJECT_DIR/Dockerfile" 
        --destination "$DESTINATION_IMAGE" 

Here, the Kaniko executor reads the Dockerfile, builds the image, and pushes it directly to the GitLab Container Registry using CI credentials. This can be attractive if your runners don’t support privileged mode or you want a more locked‑down security model.

Structuring a Real‑World Pipeline 

In a real project, you rarely only build; you usually want separate stages for buildtest, and deploy. A practical pattern is:

  1. Build and push an image tagged with the commit SHA. 

  1. Run tests against a container started from that image. 

  1. Deploy from a known, tested tag (e.g., : main or a release tag). 

For example (using Docker‑in‑Docker): 

image: docker:25.0 

services: 
  - name: docker:25.0-dind 
    command: ["--tls=false"] 

variables: 
  DOCKER_TLS_CERTDIR: "" 
  DOCKER_DRIVER: overlay2 
  IMAGE_TAG: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" 

stages: 
  - build 
  - test 
  - push 

before_script: 
  - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" 

build-image: 
  stage: build 
  script: 
    - docker build -t "$IMAGE_TAG" . 
  artifacts: 
    reports: 
      dotenv: build.env 
  after_script: 
    - echo "IMAGE_TAG=$IMAGE_TAG" >> build.env 

test-image: 
  stage: test 
  script: 
    - docker run --rm "$IMAGE_TAG" sh -c "echo 'Run your app tests here'" 
  needs: 
    - job: build-image 
      artifacts: true 

push-image: 
  stage: push 
  script: 
    - docker push "$IMAGE_TAG" 
  only: 
    - main 
  needs: 
    - job: build-image 
      artifacts: true 

This structure ensures the same image that passed tests is the one pushed to the registry, matching best practices described in GitLab and Docker documentation.

Best Practices for Building Images in GitLab CI 

When designing your pipeline, keep these recommendations in mind: 

  • Use the project’s Container Registry wherever possible for simplicity; GitLab automatically provides URL and credentials as CI variables.

  • Pin Docker and dind versions to avoid breaking changes from upstream updates (docker:25.0, docker:25.0-dind).

  • Keep Dockerfiles efficient: leverage layer caching, avoid unnecessary layers, and clean up temp files to reduce build times and image size.

  • Separate build and test stages so you can reuse the image across jobs and track failures clearly.

  • Use tags on runners (e.g., dind, docker) so only runners with proper Docker support pick up image‑build jobs.

These choices make your pipelines faster, safer, and easier to maintain over time. 

When to Choose Which Approach 

  • Use Docker‑in‑Docker if your runners can run privileged containers and you want a straightforward, official pattern aligned with GitLab docs.

  • Use Kaniko or similar tools if security policies restrict Docker daemons in CI or you run in Kubernetes‑backed environments where rootless builds are preferred.

Both approaches can reliably build and push images; the decision often comes down to security constraints and runner capabilities. 

Summary 

Building Docker images in GitLab CI comes down to three essentials: 

  1. Pick a build mechanism (Docker‑in‑Docker or a rootless builder). 

  1. Define a .gitlab-ci.yml that builds, tags, and pushes images to a registry. 

  1. Integrate this into a multi‑stage pipeline that tests and eventually deploys from the same image artefact. 

Once this is in place, every commit produces a fresh Docker image you can confidently ship, making your delivery process more consistent and automated. 

Tags: