Building Docker Images in CI¶
Overview¶
Author a GitLab CI job that builds a multi-stage Dockerfile (BuildKit-friendly), tags the image with the commit SHA, and documents promotion from registry to later environments without rebuilding.
CI builds containers so every merge produces a reproducible image. GitLab provides a Container Registry per project ($CI_REGISTRY_IMAGE). Prefer BuildKit (or Kaniko/buildah on locked-down runners) over ad-hoc Docker-in-Docker. Tag with $CI_COMMIT_SHA (and optionally a digest); promote that same image through staging and production.
This is a core tutorial in Module 8 · Docker Pipelines of the REBASH Academy GitLab CI/CD for Cloud & DevOps Engineers series — written for Cloud, DevOps, Platform, and SRE engineers.
Prerequisites¶
Learning Objectives¶
By the end of this tutorial, you will be able to:
- Sketch a Docker / BuildKit CI job using
$CI_REGISTRY_* - Write a minimal multi-stage Dockerfile
- Explain SHA tags vs floating
latest - Describe image promotion without rebuild
- Note DinD vs rootless / Kaniko trade-offs
Architecture¶
This topic’s control points and relationships are shown below.
Theory¶
What it is¶
A Docker pipeline compiles application code into an OCI image inside a GitLab job, then pushes it to a registry. Common builders:
| Builder | Typical setup | Notes |
|---|---|---|
| Docker + BuildKit | docker:cli + docker:dind service | Familiar; needs privileged or socket carefully |
| Kaniko / buildah | No daemon | Better for restricted Kubernetes runners |
| GitLab Container Registry | $CI_REGISTRY + job token | Default home for project images |
Multi-stage Dockerfiles keep build tools out of the final runtime image. Promotion means retagging or deploying the same digest — never “rebuild on main with different base layers” for production.
Why it matters¶
Laptop-built images drift from CI and skip scanners. Registry-hosted, SHA-tagged images are the unit of deploy for Kubernetes and cloud services. Layer caching and BuildKit cut minutes; digest pins stop surprise base-image moves. Without promotion discipline, staging and production silently diverge.
How it works¶
- Job authenticates to
$CI_REGISTRYwith$CI_REGISTRY_USER/$CI_REGISTRY_PASSWORD(or job token). - BuildKit builds the Dockerfile (
DOCKER_BUILDKIT=1ordocker buildx). - Tag
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA(and maybe a branch tag for non-prod). - Push; optionally record digest as an artefact or release note.
- Deploy jobs pull that SHA/digest; production never rebuilds from source for the same commit.
Cache mounts and registry pull-through caches accelerate rebuilds; they do not replace pinning base images by digest in production Dockerfiles.
Key concepts and comparisons¶
| Practice | Prefer | Avoid |
|---|---|---|
| Tags | Commit SHA / semver / digest | Only latest |
| Stages | Multi-stage slim runtime | Single fat image with compilers |
| Auth | CI job token / short-lived | Long-lived personal tokens in Git |
| Promote | Same digest across envs | Rebuild per environment |
Common pitfalls¶
- Privileged DinD on shared runners without isolation.
- Pushing
latestfrom every MR. - Baking secrets into image layers (
ENVwith API keys). - Assuming cache guarantees bit-identical images across builders.
- Rebuilding for production instead of promoting the tested digest.
Hands-on Lab¶
Create a workspace for this tutorial.
Focus: Dockerfile plus Kaniko-style GitLab CI job; prove Dockerfile locally
Step 1 – Create Dockerfile and CI build job¶
cat > Dockerfile << 'EOF'
FROM python:3.12-alpine AS runtime
WORKDIR /app
COPY app.py .
USER nobody
CMD ["python", "app.py"]
EOF
echo 'print("hello from gitlab docker lab")' > app.py
cat > .gitlab-ci.yml << 'EOF'
stages: [build]
build_image:
stage: build
image:
name: gcr.io/kaniko-project/executor:v1.23.2-debug
entrypoint: [""]
script:
- echo "Would push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
- /kaniko/executor --context "$CI_PROJECT_DIR" --dockerfile "$CI_PROJECT_DIR/Dockerfile" --destination "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" --no-push
rules:
- if: $CI_COMMIT_BRANCH
EOF
Step 2 – Local Docker build proof¶
docker build -t rebash-gitlab-lab:local .
docker run --rm rebash-gitlab-lab:local
docker rmi rebash-gitlab-lab:local
grep -E 'kaniko|--no-push' .gitlab-ci.yml
Final step – Cleanup note¶
Validation¶
- Lab commands run under
~/rebash-gitlab/module-08/ - You can explain each Theory section in your own words
- You used modern tooling where it applies to this topic
- You can describe one production failure mode for this topic
Code Walkthrough¶
Production practice for Building Docker Images in CI always combines:
- Inspect before you change (status, plan, logs, dry-run)
- Prefer reversible, documented changes (Git, IaC, drop-ins, version pins)
- Capture evidence (command output, pipeline logs) for handovers
- Prefer current tools and APIs over legacy shortcuts
- Least privilege — escalate credentials only when required
Keep runbooks short enough to follow under pressure. Automate checks; keep humans for judgement.
Security Considerations¶
- Treat credentials and tokens for gitlab as privileged — never commit them
- Prefer short-lived auth (OIDC, roles, SSO) over long-lived keys
- Validate blast radius before apply/deploy/delete operations
- Restrict who can approve production changes
- Collect audit logs; limit who can read sensitive traces
Common Mistakes¶
Privileged DinD on shared runners without isolation.
Validate assumptions against the Theory section and official docs before changing production.
Pushing latest from every MR.
Lab shortcuts (open security groups, admin roles, skip approvals) must not ship unchanged.
Changing production without a rollback path
Always know how to revert (previous artefact, prior release, state rollback, DNS failback).
Best Practices¶
- Encode Building Docker Images in CI changes as code and review them in pull requests
- Pin versions (images, modules, actions, provider plugins)
- Separate environments with clear promotion gates
- Alert on symptoms with runbooks attached
- Destroy lab resources; tag everything with owner and expiry where possible
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
| Auth / permission denied | Wrong identity, policy, or scope | Check caller identity, roles, and least-privilege policies |
| Timeout / no route | Network, DNS, security group, or endpoint | Trace path, DNS, and allow-lists before retrying |
| Drift / unexpected plan | Manual change or wrong state/workspace | Reconcile desired vs actual; avoid click-ops on managed resources |
| Pipeline/job red | Flaky step, cache, or missing secret | Read failing step logs; bisect recent workflow/config changes |
| Cost spike | Idle load balancer, NAT, oversized compute | Inventory billable resources; stop/delete labs promptly |
Summary¶
Building Docker Images in CI is essential for Cloud and DevOps engineers working with gitlab. Practise the lab until the inspection and change path is muscle memory, then continue the track.
Interview Questions¶
- Why is Docker-in-Docker often avoided in favour of Kaniko/Buildah?
- How should image tags be chosen for traceability?
- What does --no-push buy you while learning CI image builds?
- How do you keep registry credentials out of the Dockerfile?
- What base-image practices reduce supply-chain risk?
Sample answer — question 2
Check Dockerfile path/context, registry auth, and whether the executor may spawn builders. Confirm destination before enabling push.
Sample answer — question 4
Authenticate via CI variables or OIDC-linked tokens, never ENV passwords in the image. Prefer minimal bases and non-root users.