DevOpsLesson
DevOpsLesson

Free, comprehensive DevOps tutorials and learning roadmaps. Master Docker, Kubernetes, CI/CD, and more.

Stay Updated

Get notified about new tutorials and features.

Tutorials

  • What is DevOps?
  • Docker Tutorial
  • Terraform Tutorial
  • CI/CD Pipeline
  • All Tutorials

Roadmaps

  • DevOps Engineer
  • Cloud Engineer
  • SRE Path
  • All Roadmaps

Company

  • About Us
  • Blog
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 DevOpsLesson. All rights reserved.

DOCKERKUBERNETESTERRAFORMAWSCI/CDLINUXGITDEVOPS ROADMAPCLOUD ROADMAPSRE ROADMAPGIT CHEATSHEETDOCKER CHEATSHEETK8S CHEATSHEETTF CHEATSHEETLINUX CHEATSHEETDOCKERFILE LINTERYAML VALIDATORCRON PARSERREGEX TESTER

Cicd Tutorial

Introduction to CI/CD
Getting Started with GitLab CI/CD
Stages, Jobs, Artifacts & Cache
Variables, Secrets and Environments in GitLab CI
GitLab Runners
GitLab CI Pipeline Rules
GitLab CI Environments
GitLab CI Docker Workflows
GitLab CI Testing
GitLab CI Security Scanning
GitLab CI Deploy to AWS
GitLab CI Multi-Project Pipelines
GitLab CI Best Practices

GitLab CI Docker Workflows

PreviousPrev
Next

Learn GitLab CI Docker pipelines, per-job and global images, Docker-in-Docker, Kaniko, GitLab Container Registry authentication, Docker Hub pushes, tagging strategies, and a complete build and push example.

Docker and GitLab CI/CD are a natural pair. GitLab jobs often run inside containers, and many delivery workflows also need to build Docker images, tag them, and push them to a registry. That creates two related but different concepts that beginners sometimes confuse:

  1. using a Docker image as the environment for a CI job
  2. building a new Docker image inside the CI job itself

Understanding both is crucial if you want to design reliable GitLab CI Docker pipelines. This tutorial explains how the image keyword works, how to build and push images, why Docker-in-Docker exists, when it is risky, when Kaniko is a better alternative, and how to authenticate to the GitLab Container Registry and Docker Hub.

Using Docker Images in CI Jobs with image

The simplest Docker feature in GitLab CI is the image keyword. It tells GitLab which container image to use as the runtime environment for a job.

lint:
  image: node:20
  script:
    - npm ci
    - npm run lint

In this example, the job itself runs inside the node:20 image. That means Node.js and npm are already available in the execution environment.

This pattern is ideal because it avoids installing language runtimes by hand during every pipeline run. Instead, the job starts from a purpose-built container image.

Why this matters

Different jobs often need different tools:

  • Node.js for frontend tests
  • Python for backend checks
  • Docker CLI for image builds
  • Terraform for infrastructure validation

By selecting the right image per job, you keep pipelines clean, reproducible, and easier to maintain.

Global Default Image vs Per-Job Image

GitLab lets you define an image globally or on individual jobs.

Global default image

default:
  image: node:20

build:
  script:
    - npm ci
    - npm run build

test:
  script:
    - npm ci
    - npm test

This is convenient when most jobs use the same runtime.

Per-job image

default:
  image: node:20

terraform-validate:
  image: hashicorp/terraform:1.9
  script:
    - terraform validate

Use a per-job image when a specific job needs different tooling. This keeps each job minimal and precise.

When to choose each approach

  • use a global image when most jobs share the same runtime
  • use a per-job image when a job needs specialized tools
  • mix both for a clean and flexible pipeline

This design is common in mature GitLab CI Dockerfile workflows because it reduces repetition without sacrificing clarity.

Pulling Images from Docker Hub, GitLab Container Registry, and Private Registries

The image keyword can point to multiple registry sources.

Docker Hub

image: python:3.12

This is the default experience most developers know.

GitLab Container Registry

image: registry.gitlab.com/my-group/my-project/build-image:latest

This is useful when your team publishes internal base images, such as a hardened Node.js image with preinstalled tools.

Private registries

You can also pull from private registries if the runner or job has permission. Depending on your setup, that may involve runner-level Docker authentication, predefined variables, or cloud-specific registry login steps.

For enterprise DevOps teams, internal base images are often the best practice because they centralize patching, security hardening, and tool standardization.

Building a Docker Image in CI: The Real Challenge

Running a job inside a Docker image is easy. Building a new Docker image in that job is trickier.

Why? Because docker build expects access to a Docker daemon. If your GitLab job already runs in a container, it does not automatically have a Docker daemon inside it.

That is the core challenge behind GitLab CI build Docker image pipelines: Docker usually needs Docker.

This leads to the classic Docker-in-Docker pattern.

Docker-in-Docker (DinD): How It Works

Docker-in-Docker, often called DinD or dind GitLab, means your CI job talks to a separate Docker daemon running as a service container.

A standard GitLab example looks like this:

build-image:
  image: docker:27
  services:
    - docker:27-dind
  variables:
    DOCKER_HOST: tcp://docker:2375
    DOCKER_TLS_CERTDIR: ""
  script:
    - docker version
    - docker build -t myapp:$CI_COMMIT_SHA .

What is happening here?

  • image: docker:27 gives the job the Docker CLI.
  • services: docker:27-dind starts a sibling service container running the Docker daemon.
  • DOCKER_HOST tells the Docker CLI where the daemon is.
  • DOCKER_TLS_CERTDIR: "" disables the default TLS directory behavior for simpler examples.

Now the job container can execute docker build, docker tag, and docker push against the daemon in the service container.

Why DinD Is Popular

DinD remains common because it is familiar. Many developers already know local Docker commands, so reusing them in CI feels natural. It supports a wide range of image build tasks, including:

  • docker build
  • docker push
  • docker login
  • multi-stage target builds
  • standard tagging strategies

For many teams, especially those starting with GitLab CI Docker build push workflows, DinD is the first solution they learn.

Security Concerns with Docker-in-Docker

DinD is useful, but it is not always the safest choice. Many DinD setups require privileged mode on the runner. Privileged mode grants broad kernel capabilities and weakens isolation.

Why is that concerning?

  • a compromised build container has more power
  • privileged mode increases the blast radius of malicious or buggy jobs
  • production-grade multi-tenant environments should be extra careful

This does not mean DinD is always wrong. It means you should use it deliberately.

When to avoid DinD

Consider alternatives when:

  • you run untrusted workloads
  • you want stronger isolation
  • you do not want privileged runners
  • your security team restricts Docker daemon access
  • you only need to build container images, not run arbitrary Docker workflows

That is where Kaniko becomes attractive.

Kaniko as a DinD Alternative

Kaniko is a tool designed to build container images from a Dockerfile without requiring a Docker daemon. That makes it a strong option for GitLab CI Kaniko pipelines.

A basic Kaniko job looks like this:

build-image-kaniko:
  image:
    name: gcr.io/kaniko-project/executor:latest
    entrypoint: [""]
  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 "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"

Why teams like Kaniko

  • no Docker daemon required
  • works well in containerized CI environments
  • avoids some privileged-mode concerns
  • purpose-built for image building and pushing

Tradeoffs

  • different operational model than standard Docker CLI
  • some advanced Docker workflows may need adjustment
  • teams must learn Kaniko-specific commands and debugging patterns

If your main goal is just to build and push images securely, Kaniko is often a strong fit.

Pushing to GitLab Container Registry

The GitLab Container Registry integrates naturally with GitLab CI/CD. GitLab exposes predefined variables such as:

  • CI_REGISTRY
  • CI_REGISTRY_IMAGE
  • CI_REGISTRY_USER
  • CI_REGISTRY_PASSWORD

These make GitLab CI push Docker image workflows straightforward.

Docker login example

script:
  - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
  - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
  - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"

This works especially well when your source code and image destination both live in GitLab.

Why the GitLab registry is convenient

  • built into the platform
  • auth variables are already available in CI
  • image history stays close to repository history
  • good fit for GitLab-native release automation

Pushing to Docker Hub

You may also need to push to Docker Hub. In that case, store your credentials as masked CI/CD variables such as:

  • DOCKERHUB_USERNAME
  • DOCKERHUB_PASSWORD

Then authenticate during the job:

script:
  - echo "$DOCKERHUB_PASSWORD" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
  - docker build -t myorg/myapp:$CI_COMMIT_SHA .
  - docker push myorg/myapp:$CI_COMMIT_SHA

Best practices:

  • keep credentials masked and protected
  • scope access tokens to the minimum required permissions
  • avoid personal passwords when registry tokens are available

Tagging Strategies for Docker Images in GitLab CI

Good image tags make releases traceable and predictable. A few common strategies are especially useful.

Commit SHA tag

docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .

This is the most traceable tag because it points to an exact commit.

Git tag release

docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG" .

This is ideal for versioned releases such as v1.4.0.

Latest tag

docker tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" "$CI_REGISTRY_IMAGE:latest"

This is convenient, but should not be your only tag because latest is mutable and less auditable.

Recommended strategy

In many real pipelines, push more than one tag:

  • immutable CI_COMMIT_SHA
  • semantic version tag when building a release
  • latest for convenience if your deployment process expects it

This gives you both usability and traceability.

Multi-Stage Builds and --target

If your Dockerfile uses multi-stage builds, GitLab CI can build specific stages with --target.

Example Dockerfile:

FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine AS runtime
COPY --from=build /app/dist /usr/share/nginx/html

In CI, you might build only the intermediate stage for testing or caching purposes:

docker build --target build -t myapp-build:$CI_COMMIT_SHA .

Or build the full runtime image for publication:

docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .

This is useful when your pipeline separates validation from release packaging.

Complete Working Example: Build, Tag, and Push to GitLab Container Registry

Here is a full GitLab CI Docker build push example using DinD.

stages:
  - test
  - build

default:
  image: node:20

unit-tests:
  stage: test
  script:
    - npm ci
    - npm test

build-and-push-image:
  stage: build
  image: docker:27
  services:
    - docker:27-dind
  variables:
    DOCKER_HOST: tcp://docker:2375
    DOCKER_TLS_CERTDIR: ""
  before_script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
  script:
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" -t "$CI_REGISTRY_IMAGE:latest" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
    - docker push "$CI_REGISTRY_IMAGE:latest"
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

How it works

  • unit-tests runs in a normal Node.js image.
  • build-and-push-image switches to the Docker CLI image.
  • the DinD service provides a Docker daemon.
  • GitLab registry credentials come from predefined variables.
  • the image is built once and tagged twice.
  • both the immutable SHA tag and the convenience latest tag are pushed.
  • the job runs only on main.

This is a practical, production-style starting point for many teams.

Example with Kaniko Instead of DinD

If you want to avoid privileged DinD, here is a concise Kaniko version:

build-and-push-image:
  stage: build
  image:
    name: gcr.io/kaniko-project/executor:latest
    entrypoint: [""]
  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 "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"         --destination "$CI_REGISTRY_IMAGE:latest"
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

This is often easier to justify in security-conscious environments.

Common Mistakes to Avoid

  1. Confusing job image with built image. image: defines the runtime for the CI job. It does not publish your app image.
  2. Using DinD without understanding privileged mode. Security tradeoffs matter.
  3. Pushing only latest. You lose precise traceability.
  4. Hardcoding registry credentials. Use CI variables.
  5. Skipping test stages before image publication. Broken images are still broken releases.
  6. Ignoring multi-stage builds. They often reduce image size and improve security.

Final Takeaway

GitLab CI and Docker work beautifully together when you separate the concerns clearly. Use the image keyword to choose the right runtime for each job. Use DinD when you need a familiar Docker CLI workflow and accept the security implications. Use Kaniko when you want a daemonless image build approach. Authenticate safely to the GitLab Container Registry or Docker Hub with CI variables, tag images in a traceable way, and keep your Docker pipelines honest by testing before you publish.


Knowledge Check

Exercise

Question 1: The image Keyword

What does the image keyword do in a GitLab CI job?

Exercise

Question 2: DinD vs Kaniko

Why do some teams prefer Kaniko over Docker-in-Docker for GitLab CI image builds?

Exercise

Question 3: Tagging Strategy

Which image tagging approach gives the best traceability in a GitLab CI pipeline?

PreviousPrev
Next

Continue Learning

GitLab Runners

Learn what a GitLab Runner is, how GitLab CI runners execute jobs, runner types, executor choices, runner registration, tags, security, concurrency, and a full Docker executor example.

12 min read·Easy

GitLab CI Pipeline Rules

Learn GitLab CI rules, rules if, rules changes, rules exists, workflow rules, merge request pipeline conditions, manual jobs, and how to control exactly when GitLab pipelines and jobs run.

12 min read·Easy

GitLab CI Environments

Learn GitLab CI environments, deployment tracking, review apps, environment URLs, protected environments, deployment approvals, scoped variables, DORA metrics, and a complete multi-environment deployment example.

12 min read·Medium

Explore Related Topics

Do

Docker Tutorials

Build and push Docker images in your pipelines

Gi

Git Tutorials

Trigger CI/CD pipelines from Git events

Try the Tool

YAML Validator

Validate GitHub Actions and CI/CD pipeline YAML files.

Cron Parser

Build and understand scheduled workflow cron expressions.

On This Page

Using Docker Images in CI Jobs with `image`Why this mattersGlobal Default Image vs Per-Job ImageGlobal default imagePer-job imageWhen to choose each approachPulling Images from Docker Hub, GitLab Container Registry, and Private RegistriesDocker HubGitLab Container RegistryPrivate registriesBuilding a Docker Image in CI: The Real ChallengeDocker-in-Docker (DinD): How It WorksWhat is happening here?Why DinD Is PopularSecurity Concerns with Docker-in-DockerWhen to avoid DinDKaniko as a DinD AlternativeWhy teams like KanikoTradeoffsPushing to GitLab Container RegistryDocker login exampleWhy the GitLab registry is convenientPushing to Docker HubTagging Strategies for Docker Images in GitLab CICommit SHA tagGit tag releaseLatest tagRecommended strategyMulti-Stage Builds and `--target`Complete Working Example: Build, Tag, and Push to GitLab Container RegistryHow it worksExample with Kaniko Instead of DinDCommon Mistakes to AvoidFinal TakeawayKnowledge Check