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 benefit from:

  • 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 reliable, consistent 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, it executes jobs using the Docker executor. This means each job starts in a fresh container based on the image you specify. If you wish to build your own Docker images within your pipeline, you can take advantage of Docker-in-Docker (DinD). This setup allows a Docker daemon to run alongside your job, enabling you to run commands such as 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 are free to build images as often as you need. Additionally, since your build cache is stored on the runner's dedicated disk, repeated builds can reuse previous layers, significantly speeding up your pipelines.

Below is an example .gitlab-ci.yml file 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 your desired image name:
    - docker build -t my-image .
    # Optionally, you can run a quick test on the built image:
    # - docker run --rm my-image /path/to/tests

In this setup, 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 additional 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 wish 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.

As your runner's disk persists between pipelines, you can accelerate builds by reusing previous image layers as a cache. Here is how you can configure this:

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 allows your pipelines to complete more quickly over time by making use of Docker's layer caching.

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

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 detailed information about building Docker images in CI, feel free to consult the official GitLab documentation.