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 offers you an easy-to-use GitLab Runner cloud solution, designed to efficiently run your GitLab CI/CD jobs. Here’s what you can expect:

  • Unlimited CI/CD minutes: there’s no per-minute billing, so your pipelines can run whenever you need them.
  • Concurrent jobs: run multiple jobs in parallel to speed up your entire pipeline.
  • The Docker executor with Docker-in-Docker support: simplify 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: connect your first GitLab Runner and start running pipelines in just a few minutes!

When you use a Stackhero GitLab Runner, jobs are executed with the Docker executor. This means each 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 leverage Docker-in-Docker (DinD). This setup allows a Docker daemon to run alongside your job, so you can run commands like docker build and docker push directly within your pipeline.

One of the main advantages here is that your runner comes with unlimited CI/CD minutes. You can build images as often as you need. Plus, since your build cache is stored on the runner’s dedicated disk, repeated builds can reuse previous layers, which helps your pipelines complete much faster.

Here’s a sample .gitlab-ci.yml you can add to your repository. This configuration builds the Dockerfile located 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 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 any additional secrets.

Here’s 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 want to push your images to another registry (such as Docker Hub or a private registry), you can store those credentials as CI/CD variables and use them with docker login in the same way.

Since your runner’s disk persists between pipelines, you can speed up builds by reusing previous image layers as a cache. Here’s how you can set this 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’s fine if it doesn’t 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 run faster over time by making the most of Docker’s layer caching.

Your plan determines how many jobs can run at the same time. Jobs within the same stage start together, up to your concurrency limit. This means a stage with multiple independent jobs will finish as soon as the slowest job completes, instead of running all jobs one after another.

Here’s 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’d like to learn more about building Docker images in CI, feel free to check out the official GitLab documentation.