E-NO Logo
EN FR
GitLab CI Docker 6 Min Read

GitLab CI/CD pipelines for building and publishing Docker images: practical implementation guide

calendar_today Published: 2026-07-08
update Last Updated: 2026-07-08
analytics SEO Efficiency: 97%
Technical guide illustration for GitLab CI/CD pipelines for building and publishing Docker images: practical implementation guide.

Intro

Shipping Docker images through GitLab CI/CD gives you a repeatable, auditable path from code to a running container. In this guide you will set up a practical pipeline that builds and publishes images, understand Docker-in-Docker tradeoffs, enable BuildKit for speed, apply reliable tagging and caching, and pilot everything safely on a local GitLab Runner before using it in production.

Workflow Overview

A helpful production workflow is simple and explicit:

  • Developer pushes code to GitLab.
  • GitLab CI builds a Docker image with BuildKit.
  • The job logs in to the GitLab Container Registry.
  • The job tags the image with commit, branch, and optionally latest.
  • The job pushes all tags to the registry.
  • Optional: a deploy job pulls by tag and runs the container.

Prepare Project Files

  1. Create a Dockerfile optimized for caching.
# syntax=docker/dockerfile:1.7
FROM node:20-alpine AS base
WORKDIR /app
# Install prod dependencies with cached npm dir
COPY package*.json ./
RUN --mount=type=cache, target=/root/.npm npm ci --omit=dev
# Copy the rest last to maximize layer reuse
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
  1. Add a .dockerignore to keep the build context small:
node_modules
.git
.gitlab
Dockerfile*
*.log
  1. Optionally add docker-compose.yml for local test runs:
services:
  web:
    build: .
    ports:
      - "3000:3000"

Build Strategy Choices

You have two common ways to run Docker builds in GitLab CI:

A) Docker-in-Docker (DinD)

  • How it works: the job image is docker: XX and a docker: dind service runs the Docker daemon inside the job environment.
  • Pros: portable, self-contained, consistent across runners.
  • Cons: needs a privileged runner, can be slower to start, extra network hops.

B) Host Docker socket (shell or Docker executor with /var/run/docker.sock)

  • How it works: the job talks to the host Docker daemon.
  • Pros: usually faster builds, no DinD setup.
  • Cons: broader access to the host Docker, tighter runner hardening required.

Pick the one that matches your runner policy and security posture. For most teams starting out, DinD is a straightforward default. As you scale, host Docker can improve performance if you can accept the security tradeoffs.

Enable BuildKit

BuildKit speeds up builds, improves caching, and enables features like cache mounts and secrets. Enable it by setting DOCKER_BUILDKIT=1 and using docker buildx for advanced cache options.

Key tips:

  • Use the syntax directive in your Dockerfile for modern features.
  • Keep COPY of package manifests before source files to reuse dependency layers.
  • Use --mount=type=cache in RUN steps that download packages.

GitLab CI Pipeline Config

This example uses DinD, logs in to the GitLab Container Registry, applies safe tags, and pushes images. Save as .gitlab-ci.yml.

stages: [build, push]

image: docker:24

services:
  - name: docker:24-dind

variables:
  DOCKER_TLS_CERTDIR: ""
  DOCKER_DRIVER: overlay2
  DOCKER_BUILDKIT: "1"
  IMAGE: $CI_REGISTRY_IMAGE
  TAG_SHA: $CI_COMMIT_SHORT_SHA
  TAG_BRANCH: $CI_COMMIT_REF_SLUG
  TAG_LATEST: latest

before_script:
  - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"

build:
  stage: build
  script:
    - docker info
    - docker buildx create --name ci-builder --use || true
    - docker buildx inspect --bootstrap
    - >
      docker buildx build \
        --pull \
        --progress=plain \
        --tag "$IMAGE:$TAG_SHA" \
        --tag "$IMAGE:$TAG_BRANCH" \
        --cache-from=type=registry, ref="$IMAGE:buildcache" \
        --cache-to=type=registry, ref="$IMAGE:buildcache",mode=max \
        --file Dockerfile . \
        --load

push:
  stage: push
  script:
    - docker push "$IMAGE:$TAG_SHA"
    - docker push "$IMAGE:$TAG_BRANCH"
    - |
      if [ "$CI_COMMIT_REF_NAME" = "main" ] || [ "$CI_COMMIT_REF_NAME" = "master" ]; then
        docker tag "$IMAGE:$TAG_SHA" "$IMAGE:$TAG_LATEST"
        docker push "$IMAGE:$TAG_LATEST"
      fi

Notes:

  • $CI_REGISTRY, $CI_REGISTRY_IMAGE, $CI_REGISTRY_USER, and $CI_REGISTRY_PASSWORD are provided by GitLab when the Container Registry is enabled on the project.
  • The first build will not have a cache image; BuildKit will warn but continue.
  • If using host Docker instead of DinD, remove services, ensure your runner has Docker available, and keep the same script steps.

Cache Strategy

Layer caching is your main lever for faster builds.

Practical steps:

  • Order Dockerfile instructions to maximize reuse: dependency install first, then app code.
  • Use BuildKit cache mounts for package managers.
  • Publish and pull a registry cache with buildx --cache-to and --cache-from.
  • Keep the build context lean using .dockerignore to avoid invalidating the cache.

Example of cache args already included above:

--cache-from=type=registry, ref="$IMAGE:buildcache"
--cache-to=type=registry, ref="$IMAGE:buildcache",mode=max

Local Pilot Plan

Start with a small, easy-to-measure pilot so you can verify everything locally before rollout.

Pilot scope:

  • One small service with a simple Dockerfile.
  • Success metrics: build time, image size, container start time, ability to push and pull.

Steps:

  1. Build locally with BuildKit:
DOCKER_BUILDKIT=1 docker build -t app: dev .
  1. Run locally with Docker Compose:
docker compose up --build

Visit http://localhost:3000 to confirm it runs.

  1. Install a local GitLab Runner and run the build job:
gitlab-runner exec docker build   # docker executor
# or
gitlab-runner exec shell build    # shell executor with host Docker
  1. Push a test branch to GitLab and confirm the image appears in the project registry with the expected tags.

Keep the pilot narrow, measurable, and easy to inspect. Expand to more services only after you meet the metrics.

Troubleshooting and Pitfalls

Common issues and quick fixes:

  • Login failed: Ensure the project registry is enabled and that $CI_REGISTRY_USER and $CI_REGISTRY_PASSWORD are present. For protected branches, confirm permissions.
  • DinD requires privileged runner: Make sure the runner is configured to allow privileged containers if builds fail with daemon errors.
  • No cache on first build: Safe to ignore. The second build should be faster if tags are the same and the context is stable.
  • Slow builds: Add or improve .dockerignore, keep COPY order optimized, and use BuildKit cache mounts. Consider host Docker if allowed.
  • Tag collisions: Use commit SHA for immutable tags. Use latest only on main or master.
  • Large images: Switch to alpine or distroless bases where possible, and keep build artifacts out of the final image.
  • Secrets in images: Do not bake secrets into layers. Use build args or runtime env vars and verify with docker history.

Conclusion

You now have a practical GitLab CI pipeline that builds and publishes Docker images with sensible tags, BuildKit acceleration, and a cache strategy. Start with the local pilot, verify the metrics, then scale to more services. Revisit your build strategy choice (DinD vs host Docker) as you grow, and keep the Dockerfile lean to preserve fast, reliable builds.

Article Quality Score

Reader usefulness 97%
  • check_circle Reader-ready guide
  • check_circle Practical examples included
  • check_circle Clean SEO article URL