CI/CDGitLabDocker

Building a Production-Ready GitLab CI Pipeline with Docker in 2026

22 July 2026 · Sphinx-Digital

A GitLab CI pipeline that just runs docker build && docker push is not a CI pipeline — it is a deployment script with a YAML wrapper. A production-ready pipeline builds trust: it tells you whether this change is safe to promote, not just whether it compiles.

Here is the pipeline structure we use after running dozens of CI/CD training sessions.

Pipeline Stages

stages:
  - build
  - test
  - security
  - publish
  - deploy

Each stage runs only if the previous one passes. This is not obvious to beginners: by default in GitLab CI, all jobs in a stage run in parallel, and the pipeline moves to the next stage only when all jobs in the current stage succeed.

Build: Multi-Stage Docker for Speed

build:image:
  stage: build
  image: docker:26
  services:
    - docker:26-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  script:
    - docker build
        --cache-from $CI_REGISTRY_IMAGE:cache
        --build-arg BUILDKIT_INLINE_CACHE=1
        -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
        -t $CI_REGISTRY_IMAGE:cache
        .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE:cache
  rules:
    - if: $CI_COMMIT_BRANCH

The --cache-from trick cuts build time from 4 minutes to 40 seconds on a warm cache. The cache tag always points to the latest built image, so the next build has something to pull from.

Test: Unit + Integration in Parallel

test:unit:
  stage: test
  image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  script:
    - pytest tests/unit/ -v --junitxml=report.xml
  artifacts:
    reports:
      junit: report.xml
    expire_in: 1 week

test:integration:
  stage: test
  image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  services:
    - postgres:16-alpine
  variables:
    DATABASE_URL: postgresql://test:test@postgres/testdb
    POSTGRES_USER: test
    POSTGRES_PASSWORD: test
    POSTGRES_DB: testdb
  script:
    - pytest tests/integration/ -v

JUnit report artifacts are displayed natively in GitLab’s merge request UI. Engineers see which tests failed without reading the pipeline logs.

Security: Scan Before You Push to Production

security:trivy:
  stage: security
  image:
    name: aquasec/trivy:latest
    entrypoint: [""]
  script:
    - trivy image
        --exit-code 1
        --severity CRITICAL
        --no-progress
        $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  allow_failure: false   # Block on CRITICAL CVEs

security:sast:
  stage: security
  include:
    - template: Security/SAST.gitlab-ci.yml

--exit-code 1 with --severity CRITICAL fails the pipeline if a CRITICAL CVE is found in the image. This is the floor — you can tighten it to HIGH,CRITICAL once the team has worked through the initial findings.

Publish: Tag on Merge to Main

publish:latest:
  stage: publish
  image: docker:26
  services:
    - docker:26-dind
  script:
    - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest
    - docker push $CI_REGISTRY_IMAGE:latest
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Deploy: Environment Promotion with Manual Gates

deploy:staging:
  stage: deploy
  environment:
    name: staging
    url: https://staging.myapp.com
  script:
    - kubectl set image deployment/api api=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

deploy:production:
  stage: deploy
  environment:
    name: production
    url: https://myapp.com
  when: manual        # Human clicks the button
  script:
    - kubectl set image deployment/api api=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

The when: manual gate means production deployments require a human click in the GitLab UI. GitLab’s environment view shows current deployment status per environment — this is what you show in the sprint review.

Training

The CI/CD training at Sphinx-Digital builds exactly this pipeline from scratch over 2 days, starting from a blank .gitlab-ci.yml and ending with a full environment promotion flow including rollback strategies.