GitLab CI doesn’t actually build your Docker images; it orchestrates the process using external tools like Docker itself.
Let’s see it in action. Imagine you have a simple Node.js app.
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "start"]
Here’s how you might build and push this in a .gitlab-ci.yml file:
# .gitlab-ci.yml
stages:
- build
- push
variables:
DOCKER_REGISTRY: registry.gitlab.com
IMAGE_NAME: $CI_PROJECT_PATH/$CI_COMMIT_REF_SLUG
IMAGE_TAG: $CI_COMMIT_SHA
build_image:
stage: build
image: docker:20.10.16
services:
- docker:20.10.16-dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $IMAGE_NAME:$IMAGE_TAG .
- docker tag $IMAGE_NAME:$CI_COMMIT_SHA $IMAGE_NAME:latest
when: on_branch # Only build on branches, not merge requests
push_image:
stage: push
image: docker:20.10.16
services:
- docker:20.10.16-dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker push $IMAGE_NAME:$IMAGE_TAG
- docker push $IMAGE_NAME:latest
needs:
- build_image
only:
- main # Only push for the main branch
This pipeline uses the official docker image and the docker:dind (Docker-in-Docker) service. The build_image job logs into your GitLab container registry, builds the image using your Dockerfile, and tags it with the commit SHA and latest. The push_image job then pushes both of these tags to the registry.
The core idea is that GitLab CI acts as the conductor. It spins up the necessary Docker environment (thanks to docker:dind), executes your docker build command, and then uses the built-in CI/CD variables like $CI_REGISTRY_USER, $CI_REGISTRY_PASSWORD, and $CI_REGISTRY to authenticate and push. The $CI_COMMIT_SHA is a fantastic way to ensure immutability; you always know exactly which commit produced a given image.
The docker:dind service is crucial here. It starts a separate Docker daemon that your CI job can interact with. Without it, the docker build commands within the job wouldn’t have a Docker daemon to talk to. The image: docker:20.10.16 line specifies the client tool you’ll use to issue commands to that daemon.
You might notice $CI_PROJECT_PATH/$CI_COMMIT_REF_SLUG. $CI_PROJECT_REF_SLUG is a URL-friendly version of your branch or tag name. This means if you’re on the develop branch, your image name might become your-group/your-project:develop. Using $CI_COMMIT_SHA for the primary tag is generally preferred for production deployments because it’s a unique, immutable identifier for that specific build. The latest tag is convenient but can be ambiguous.
The needs keyword in push_image ensures that the push job only runs after build_image has successfully completed. The only: - main directive restricts the push to only happen when commits are made directly to the main branch, preventing accidental pushes from feature branches.
A common pattern is to build and push on every commit to a branch, but only deploy from specific tags. This pipeline gives you the foundation for that. You can extend this by adding a deploy stage that pulls a specific tag from the registry and deploys it.
GitLab’s built-in container registry is convenient, but you can also configure this pipeline to push to external registries like Docker Hub or AWS ECR by changing the DOCKER_REGISTRY variable and providing different authentication credentials.
The docker login command uses predefined GitLab CI/CD variables. $CI_REGISTRY_USER and $CI_REGISTRY_PASSWORD are automatically provided by GitLab when you use its built-in registry. They represent credentials for the user running the pipeline.
You’ll likely want to explore how to use these images in subsequent deployment jobs.