GitLab Runner: Building Docker images

Build and push Docker images from your GitLab CI/CD pipelines using your Stackhero runner and Docker-in-Docker

👋 Welcome to the Stackhero documentation!

Stackhero gives you an easy-to-use GitLab Runner cloud solution designed to handle your GitLab CI/CD jobs efficiently. Here is what you can look forward to:

  • Unlimited CI/CD minutes: there is no per-minute billing, so your pipelines can run whenever you need them.
  • Multiple concurrent jobs: run several jobs at the same time to speed up your entire pipeline.
  • The Docker executor with Docker-in-Docker support: streamline building and pushing your container images.
  • Compatible with GitLab.com as well as any self-managed GitLab instance.
  • A private, dedicated VM powered by fast NVMe/SSD disks for consistent, reliable builds.
  • Available in both 🇪🇺 Europe and 🇺🇸 USA regions.

Save time: you can connect your first GitLab Runner and start running pipelines in just a few minutes!

When you use a Stackhero GitLab Runner, it runs jobs with the Docker executor. This means every job starts in a fresh container based on the image you specify. If you want to build your own Docker images as part of your pipeline, you can take advantage of Docker-in-Docker (DinD). This setup lets a Docker daemon run alongside your job, so you are able to run commands like docker build and docker push directly within your pipeline.

One of the great benefits here is that your runner comes with unlimited CI/CD minutes. You are free to build images as often as you like. Plus, since your build cache lives on the runner's dedicated disk, repeated builds can reuse previous layers, helping your pipelines finish much faster.

Here is a sample .gitlab-ci.yml you can add to your repository. This setup builds the Dockerfile found at the root of your project:

build-image:
  stage: build
  image: docker:27
  services:
    - docker:27-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - docker info
  script:
    # Replace "my-image" with the name you want:
    - docker build -t my-image .
    # Optionally, you can run a quick smoke test on the built image:
    # - docker run --rm my-image /path/to/tests

In this configuration, the docker:27-dind service starts up the Docker daemon. The variable DOCKER_TLS_CERTDIR: "/certs" enables a secure TLS connection between your job and the Docker daemon.

GitLab provides several predefined variables (CI_REGISTRY, CI_REGISTRY_USER, CI_REGISTRY_PASSWORD, CI_REGISTRY_IMAGE) so your pipeline can log in and push images to the project's container registry without needing extra secrets.

Here is an example job that builds and pushes your image:

build-and-push:
  stage: build
  image: docker:27
  services:
    - docker:27-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
  script:
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
    # If you are on the default branch, you can also tag and push "latest":
    - |
      if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
        docker tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" "$CI_REGISTRY_IMAGE:latest"
        docker push "$CI_REGISTRY_IMAGE:latest"
      fi

If you would like to push your images to a different registry (like Docker Hub or a private registry), you can store those credentials as CI/CD variables and use them with docker login in a similar way.

Since your runner's disk persists between pipelines, you can speed up builds by reusing previous image layers as a cache. Here is how you can set that up:

build-cached:
  stage: build
  image: docker:27
  services:
    - docker:27-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
  script:
    # Try pulling the latest image to seed the cache (it is okay if it does not exist yet):
    - docker pull "$CI_REGISTRY_IMAGE:latest" || true
    - docker build --cache-from "$CI_REGISTRY_IMAGE:latest" -t "$CI_REGISTRY_IMAGE:latest" .
    - docker push "$CI_REGISTRY_IMAGE:latest"

This approach helps your pipelines finish faster over time by making the most of Docker's layer caching.

Your plan controls how many jobs can run at the same time. Jobs within the same stage will start together, up to your concurrency limit. This means a stage with multiple independent jobs can finish as soon as the slowest job completes, rather than running all jobs sequentially.

Here is a simple example:

stages:
  - test

unit:
  stage: test
  image: node:22
  script: npm run test:unit

integration:
  stage: test
  image: node:22
  script: npm run test:integration

e2e:
  stage: test
  image: node:22
  script: npm run test:e2e

If you set your concurrency to 3 or higher, the unit, integration, and e2e jobs will all run at the same time.

If you would like more in-depth information about building Docker images in CI, feel free to check out the official GitLab documentation.