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!
Introduction
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.
Building a Docker image with Docker-in-Docker
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.
Pushing to the GitLab container registry
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.
Speeding up repeat builds
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.
Running jobs in parallel
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.