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 a ready-to-use GitLab Runner cloud solution that runs your GitLab CI/CD jobs, with benefits including:

  • Unlimited CI/CD minutes: no per-minute billing, run your pipelines around the clock.
  • Dozens of concurrent jobs so a whole pipeline stage runs at once.
  • The Docker executor with Docker-in-Docker support to build and push your images.
  • Works with GitLab.com and any self-managed GitLab.
  • A private and dedicated VM with NVMe/SSD disks for fast, consistent builds.
  • Available in 🇪🇺 Europe and 🇺🇸 USA.

Save time: it only takes a few minutes to connect your first GitLab Runner and start running pipelines!

Your Stackhero GitLab Runner uses the Docker executor: every job runs in a clean container based on the image you choose. To build your own images inside a pipeline, you use Docker-in-Docker (DinD), where a Docker daemon runs alongside your job so you can run docker build and docker push.

Because your runner has unlimited CI/CD minutes, you can build images as often as you need, and because the build cache lives on the runner dedicated disk, repeat builds reuse previous layers and finish much faster.

Add the following .gitlab-ci.yml to your repository. It builds the Dockerfile 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 run a quick smoke test on the built image:
    # - docker run --rm my-image /path/to/tests

The docker:27-dind service starts the Docker daemon, and DOCKER_TLS_CERTDIR: "/certs" enables the secure TLS connection between your job and that daemon.

GitLab exposes a few predefined variables (CI_REGISTRY, CI_REGISTRY_USER, CI_REGISTRY_PASSWORD, CI_REGISTRY_IMAGE) so a job can log in and push to the project Container Registry with no extra secret to manage:

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"
    # Also tag and push "latest" on the default branch:
    - |
      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

To push to another registry (Docker Hub, a registry on your own infrastructure, etc.), store its credentials as CI/CD variables and use them in docker login instead.

Your runner keeps its disk between pipelines, so you can reuse the layers of a previous build to make the next one faster:

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:
    # Pull the previous image to seed the layer cache (ignore the error on the first build):
    - 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"

Your plan defines how many jobs run at the same time. Jobs in the same stage start together, up to your concurrency limit, so a stage with several independent jobs finishes in the time of its slowest job instead of running them one after another:

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

With a concurrency of 3 or more, unit, integration and e2e all run at once.

For more details on building images in CI, see the official GitLab documentation.