This article shows how to configure a Gitlab CI/CI pipeline job to automatically build & publish an OCI image from a Dockerfile when a git tag is pushed to the repository.
Introduction
The first quarter of the 21st century has significant advances in the software development workflows. Git & CI/CD, in particular, have enhanced the overall quality & performance of software development workflows, thus catapulting the ability of projects to test, integrate, build & deploy software at the same time that it is coded.
Git & CI/CD
Git provides a mature collaborative version control platform with clear development environment and release methodology.
Most git-enabled code repositories (like Github, Gitlab & Codeberg) have some form of CI deployment functionality. These deployment features allow simple configuration of jobs to run continuous actions on the code. Most of these repositories also offer generous free tiers which include ample cloud-based resource to run such jobs.
Standardized Application-Level Virtualization
Application-level virtualization has recently seen mass adoption due to the clear benefits in security and ease of deployment of apps in containers. The recent standardization of containers space through the OCI has further boosted the adoption of containerized workloads - both in homelabs & global enterprises alike. This standardization of application virtualization has enabled cascaded creation new OCI images based on other images, bringing greater control of this space to the administrator user.
OCI images can be easily built through configuration in self-documenting files (Dockerfile
) with directives to build the image step-by-step from a reference starter image.
The Opportunity - Automated OCI Images
This convergence of mature technologies - Git, CI/CD & OCI - presents an interesting opportunity for small scale deployments like home labs where deviations from existing OCI images may be required to address application-specific needs. However due to a potential lack of resources (or indeed, the need) to deploy in-house CI/CD resources to continuously build OCI images.
The Objective - Controlled Building of OCI Images on the Gitlab Cloud
This article shows an example CI/CD configuration for building an OCI image. This build process is triggered when a Git tag is pushed to a Gitlab repository. This job also adds an OCI image tag, of the same name as the git tag, to the newly built image. Finally, this new image is pushed to pre-defined container repositories on Gitlab's own container registry as well as on Docker Hub.
Approach Used to Automate OCI Image Builds
Since the objective is to build, tag & push images to remote repositories, it can be split into these three broad steps.
- Build
- Tag
- Push
Step 1: Building the OCI Image
A typical trigger for the building of images for specific apps would be either the upstream image or the app itself updates. Since the Dockerfile is simply a text file with a set of directives to create the image, this can be committed to a remote repository and changes to this repository can be used to trigger an automated build of images.
The build job can be further controlled by restricting the triggering of this pipeline only when git tags are pushed to the repository. This will prevent unnecessary build jobs from triggering on every 'push' to the remote repository.
Step 2: Tagging the OCI Image
This CD pipeline is also configured to tag the OCI image with a tag name identical to the git tag that triggers the build process.
Step 3: Pushing the OCI Image to One or More Container Registries
The final step in the build pipeline is to push this newly build image to one or more container registries. The example covered in this article shows how to push this new image to Gitlab's own container registry as well as Docker Hub.
Use of Build Environment Variables
Most container registries require active accounts for the ability to push OCI images for public or private image hosting. Hence, to be able to push the newly built image to any remote registry through automated CI/CD jobs, the associated credentials need to be available to the build environment. This can be done through the use of build environment variables that are used to store confidential credentials for repositories.
Pre-Requisites for Automating OCI Image Builds on Gitlab
Before an automated build job can be triggered in a CI/CD pipeline, some pre-requisites have to be met to ensure that the build environments are equipped to complete the task.
Registry Credentials for Pushing Images
Typically, if the target image is to be deployed to Gitlab's own registry, the registry credentials are stored in built-in variables (e.g., ${CI_REGISTRY}
, ${CI_REGISTRY_USER}
& ${CI_REGISTRY_PASSWORD}
). However if the built image is to be deployed on an external registry (or registries), the credentials have to be passed on to the build environment through environment variables, such the no user interaction is required.
Gitlab allows passing variables to build environments through "project variables" in the projects settings through the standard Gitlab web interface.
Navigate to Project Settings
› CI/CD
› Variables
› Variables

Add the following project variables.
Variable | Description | Example |
---|---|---|
${CI_REGISTRY_DH} |
Target registry | docker.io |
${CI_REGISTRY_USER_DH} |
Username for target registry | username |
${CI_REGISTRY_PASSWORD_DH} |
Password for target registry | secret |
Gitlab CI/CD Pipeline Configuration for Automated OCI Image Builds
This example only requires a minimum of two files
Dockerfile
contains the image configuration.gitlab-ci.yml
contains the CI/CD pipeline configuration
This section covers two example configurations for a CI/CD pipeline.
- A minimal configuration to build, tag & push image to Gitlab & Docker Hub.
- A standards-compliant configuration to build the image with standard OCI image labels; then tag & push the image to Gitlab & Docker Hub.
Minimal Configuration
The following example shows a minimal configuration for Gitlab CI which achieves the following, whenever a Git tag is pushed to the repository.
- Attempts to pull the 'latest' tag of the same image. This has the advantage of potentially avoiding having to build all layers of the image. If the image is not available, the process continues.
- Builds the image from
Dockerfile
. - Tags the built OCI image with the same Git tag.
- Tags the built OCI image with the
latest
tag. - Logs into the Gitlab registry.
- Pushes both tags of the image to the associated container repository on Gitlab registry.
- Logs out of the Gitlab registry.
- Tags the image with the Git tag and the 'latest' tag in the context of the Docker Hub repository.
- Logs into the Docker Hub registry.
- Pushes both tags of the image to the associated container repository on Docker Hub registry.
- Logs out of the Docker Hub registry.
image: "docker:28"
stages:
- build-tag-push
variables:
services:
- docker:dind
Build-Tag-Push:
stage: build-tag-push
only:
- tags
script:
- docker pull ${CI_REGISTRY_IMAGE}:latest || true
- >
docker build
--pull
--cache-from ${CI_REGISTRY_IMAGE}:latest
--tag ${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_NAME}
--tag ${CI_REGISTRY_IMAGE}:latest
- docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
- docker image push ${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_NAME}
- docker image push ${CI_REGISTRY_IMAGE}:latest
- docker logout
- docker image tag ${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_NAME} docker.io/cerebralvoyage/ungit-docker:${CI_COMMIT_REF_NAME}
- docker image tag ${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_NAME} docker.io/cerebralvoyage/ungit-docker:latest
- docker login -u ${CI_REGISTRY_USER_DH} -p ${CI_REGISTRY_PASSWORD_DH} ${CI_REGISTRY_DH}
- docker image push docker.io/cerebralvoyage/ungit-docker:${CI_COMMIT_REF_NAME}
- docker image push docker.io/cerebralvoyage/ungit-docker:latest
- docker logout
Full & Compliant Configuration
The following example shows a detailed configuration for Gitlab CI/CD which achieves the following, whenever a Git tag is pushed to the repository:
- Attempts to pull the 'latest' tag of the same image. This has the advantage of potentially avoiding having to build all layers of the image. If the image is not available, the process continues.
- Builds the image from
Dockerfile
. - Sets OCI standard labels to the image.
- Sets internal application-specific labels (e.g., upstream version, etc.)
- Tags the built OCI image with the same Git tag.
- Tags the built OCI image with the
latest
tag. - Logs into the Gitlab registry.
- Pushes both tags of the image to the associated container repository on Gitlab registry.
- Logs out of the Gitlab registry.
- Tags the image with the Git tag and the 'latest' tag in the context of the Docker Hub repository.
- Logs into the Docker Hub registry.
- Pushes both tags of the image to the associated container repository on Docker Hub registry.
- Logs out of the Docker Hub registry.
# Modified 7-Aug-2025
image: "docker:28"
stages:
- build-tag-push
variables:
sr_SWVersion: "1.5.28"
sr_SWRev: "d6f3ba03c7f67b565219f384ec40f623fd8762db"
# ==== Source Image ====
sr_SourceImageDigest: "sha256:1b2479dd35a99687d6638f5976fd235e26c5b37e8122f786fcd5fe231d63de5b"
sr_SourceImageTag: "22.18.0-alpine3.22"
sr_SourceImageName: "node"
sr_SourceImageRepo: "library"
sr_SourceImageRegistry: "docker.io"
# === Alpine ===
sr_AlpineVersion: "3.22.1"
# === GPG Agent ===
sr_GPGAgentVersion: "2.4.7"
# === Git ===
sr_GitVersion: "2.49.1"
# === Node ===
sr_NodeVersion: "22.18.0"
# === Ungit Docker ===
sr_DockerfileVersion: "1.2.0"
# ==== Target Image ====
sr_TargetImageRef_Gitlab: "${CI_REGISTRY_IMAGE}" # built-in variable points to the Gitlab container registry image reference
sr_TargetImageRef_DockerHub: "docker.io/cerebralvoyage/ungit-docker"
sr_TargetImageTitle: "Ungit Docker"
sr_TargetImageAuthor: "Viharm <[email protected]>"
sr_TargetImageVendor: "Cerebral Voyage"
sr_TargetImageDescr: "Ungit, the easiest way to use git. Now containerized"
sr_TargetImageURL: "https://gitlab.com/cerebral.voyage/ungit-docker"
sr_TargetImageLic: "MIT" # NU # https://spdx.org/licenses/
sr_DockerfileDir: "."
# ==== Calculated ====
sr_SourceImageRef: "${sr_SourceImageRegistry}/${sr_SourceImageRepo}/${sr_SourceImageName}:${sr_SourceImageTag}"
sr_TargetImageTag: "${CI_COMMIT_REF_NAME}"
sr_DateTimeISO: "${CI_COMMIT_TIMESTAMP}"
services:
- docker:dind
Build-Tag-Push:
stage: build-tag-push
only:
- tags
script:
- docker pull $CI_REGISTRY_IMAGE:latest || true
- >
docker build
--pull
--platform linux/amd64
--cache-from $CI_REGISTRY_IMAGE:latest
--label "org.opencontainers.image.created=${sr_DateTimeISO}"
--label "org.label-schema.build-date=${sr_DateTimeISO}"
--label "org.opencontainers.image.authors=${sr_TargetImageAuthor}"
--label "org.opencontainers.image.url=${sr_TargetImageURL}"
--label "org.label-schema.url=${sr_TargetImageURL}"
--label "org.opencontainers.image.documentation=${sr_TargetImageURL}"
--label "org.label-schema.usage=${sr_TargetImageURL}"
--label "org.opencontainers.image.source=${sr_TargetImageURL}"
--label "org.label-schema.vcs-url=${sr_TargetImageURL}"
--label "org.opencontainers.image.version=${sr_SWVersion}"
--label "org.label-schema.version=${sr_SWVersion}"
--label "org.opencontainers.image.revision=${sr_SWRev}"
--label "org.label-schema.vcs-ref=${sr_SWRev}"
--label "org.opencontainers.image.vendor=${sr_TargetImageVendor}"
--label "org.label-schema.vendor=${sr_TargetImageVendor}"
--label "org.opencontainers.image.ref.name=${sr_TargetImageRef_Gitlab}"
--label "org.opencontainers.image.title=${sr_TargetImageTitle}"
--label "org.label-schema.name=${sr_TargetImageTitle}"
--label "org.opencontainers.image.description=${sr_TargetImageDescr}"
--label "org.label-schema.description=${sr_TargetImageDescr}"
--label "org.opencontainers.image.base.digest=${sr_SourceImageDigest}"
--label "org.opencontainers.image.base.name=${sr_SourceImageRef}"
--label "voyage.cerebral.alpine.version=${sr_AlpineVersion}"
--label "voyage.cerebral.node.version=${sr_NodeVersion}"
--label "voyage.cerebral.git.version=${sr_GitVersion}"
--label "voyage.cerebral.gpg-agent.version=${sr_GPGAgentVersion}"
--label "voyage.cerebral.dockerfile.version=${sr_DockerfileVersion}"
--label "voyage.cerebral.license=${sr_TargetImageLic}"
--tag ${sr_TargetImageRef_Gitlab}:${sr_TargetImageTag}
--tag ${sr_TargetImageRef_Gitlab}:latest
"${sr_DockerfileDir}"
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker image push ${sr_TargetImageRef_Gitlab}:${sr_TargetImageTag}
- docker image push ${sr_TargetImageRef_Gitlab}:latest
- docker logout
- docker image tag ${sr_TargetImageRef_Gitlab}:${sr_TargetImageTag} ${sr_TargetImageRef_DockerHub}:${sr_TargetImageTag}
- docker image tag ${sr_TargetImageRef_Gitlab}:${sr_TargetImageTag} ${sr_TargetImageRef_DockerHub}:latest
- docker login -u $CI_REGISTRY_USER_DH -p $CI_REGISTRY_PASSWORD_DH $CI_REGISTRY_DH
- docker image push ${sr_TargetImageRef_DockerHub}:${sr_TargetImageTag}
- docker image push ${sr_TargetImageRef_DockerHub}:latest
- docker logout
# Reference: Florent CHAUVEAU <[email protected]>
# Mentioned here: https://blog.callr.tech/building-docker-images-with-gitlab-ci-best-practices/
Usage
The CI/CD configuration creates a pipeline & runs the configured job whenever a new Git tag is pushed to the Git repository.

Each Gitlab pipeline consists of only one stage (as configured in the .gitlab-ci.yml
).

On completion of the CI/CD job, the image is published to and available on the Gitlab container registry.

Conclusion
With the advent of dev-ops technologies like git, virtualization & CI/CD, automated deployment of container images is now within arms reach for home lab administrators & self-hosters, not just enterprises with resource-rich IT department. This empowers the regular self-hoster to concurrently build & deploye application container images as the dev-ops process progresses, and integrate the CI/CD pipeline into their routine workflow.
This article has taken a well-established workflow of CI/CD in a development process & shown how to deploy this workflow for a small (or large) scale dev-ops environment.
The example in this guide triggers the building of an OCI image when a git tag is pushed to the remote Gitlab repository. Once built, the image is tagged to mirror the git tag, and then published on two container repositories - one on the Gitlab registry & the other on Docker Hub.
It is hoped that this covers most scenarios of simple & small scale deployments used by home lab administrators and self-hosters.
Future Work
The CI/CD approach used in this article can be further extended to suit various common scenarious as well as niche applications. Some of these additional functionalities are as follows.
- Customization of the pipeline trigger. E.g., use specific branches.
- Customization of the image metadata. E.g., additional labels, tags, etc.
- Customization of target registries. E.g., add or replace remote registries.
Feature image © Cerebral Voyage; generated by Microsoft Copilot
Update 1 • 20-Aug-2025
- Minor typos
- Consistent visual formatting of info boxes & call-outs.