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