Learning Guides
Menu

CI/CD Integration

9 min readDocker for Developers

CI/CD Integration

Integrating Docker into your CI/CD pipeline ensures consistent builds, automated testing, and reliable deployments. This chapter covers building and testing Docker images in popular CI systems and deploying to various environments.

CI/CD Pipeline Overview

A typical Docker CI/CD pipeline:

PLAINTEXT
┌─────────────────────────────────────────────────────────┐
│                    CI/CD Pipeline                        │
│                                                          │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐    │
│  │  Build  │─▶│  Test   │─▶│  Push   │─▶│ Deploy  │    │
│  │  Image  │  │  Image  │  │  Image  │  │         │    │
│  └─────────┘  └─────────┘  └─────────┘  └─────────┘    │
│       │            │            │            │          │
│       ▼            ▼            ▼            ▼          │
│  Dockerfile    Tests in    Registry    Production      │
│  + Context    Container    (Hub/ECR)   Environment     │
└─────────────────────────────────────────────────────────┘

Pipeline Stages

StagePurposeOutcome
BuildCreate image from DockerfileImage artifact
TestRun tests inside containerPass/fail status
ScanCheck for vulnerabilitiesSecurity report
PushUpload to registryStored image
DeployUpdate running servicesNew version live

GitHub Actions

Basic Build and Push

YAML
# .github/workflows/docker.yml
name: Docker Build and Push
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
jobs:
  build:
    runs-on: ubuntu-latest
 
    steps:
      - name: Checkout
        uses: actions/checkout@v4
 
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
 
      - name: Login to Docker Hub
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: |
            ${{ secrets.DOCKERHUB_USERNAME }}/myapp:latest
            ${{ secrets.DOCKERHUB_USERNAME }}/myapp:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Multi-Platform Build

YAML
name: Multi-Platform Build
 
on:
  push:
    tags: ["v*"]
 
jobs:
  build:
    runs-on: ubuntu-latest
 
    steps:
      - name: Checkout
        uses: actions/checkout@v4
 
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
 
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
 
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 
      - name: Extract version
        id: version
        run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
 
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: |
            myuser/myapp:${{ steps.version.outputs.VERSION }}
            myuser/myapp:latest

Complete CI Pipeline

YAML
name: CI Pipeline
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
 
env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
 
jobs:
  # Build and test
  build:
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
 
    steps:
      - uses: actions/checkout@v4
 
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
 
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha
            type=ref,event=branch
            type=semver,pattern={{version}}
 
      - name: Build test image
        uses: docker/build-push-action@v5
        with:
          context: .
          target: test
          load: true
          tags: ${{ env.IMAGE_NAME }}:test
          cache-from: type=gha
          cache-to: type=gha,mode=max
 
      - name: Run tests
        run: |
          docker run --rm ${{ env.IMAGE_NAME }}:test npm test
 
      - name: Login to Container Registry
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - name: Build and push
        if: github.event_name != 'pull_request'
        uses: docker/build-push-action@v5
        with:
          context: .
          target: production
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
 
  # Security scanning
  security:
    needs: build
    runs-on: ubuntu-latest
    if: github.event_name != 'pull_request'
 
    steps:
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
          format: "sarif"
          output: "trivy-results.sarif"
          severity: "CRITICAL,HIGH"
 
      - name: Upload Trivy scan results
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: "trivy-results.sarif"
 
  # Deploy to staging
  deploy-staging:
    needs: [build, security]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    environment: staging
 
    steps:
      - name: Deploy to staging
        run: |
          # Your deployment script here
          echo "Deploying to staging..."
 
  # Deploy to production
  deploy-production:
    needs: [build, security]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production
 
    steps:
      - name: Deploy to production
        run: |
          # Your deployment script here
          echo "Deploying to production..."

Note

Use GitHub Environments for deployment approvals and secrets management. The environment key enables environment-specific secrets and protection rules.

GitLab CI

Basic Pipeline

YAML
# .gitlab-ci.yml
stages:
  - build
  - test
  - scan
  - push
  - deploy
 
variables:
  DOCKER_TLS_CERTDIR: "/certs"
  IMAGE_NAME: $CI_REGISTRY_IMAGE
 
build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker build -t $IMAGE_NAME:$CI_COMMIT_SHA .
    - docker save $IMAGE_NAME:$CI_COMMIT_SHA > image.tar
  artifacts:
    paths:
      - image.tar
    expire_in: 1 hour
 
test:
  stage: test
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker load < image.tar
    - docker run --rm $IMAGE_NAME:$CI_COMMIT_SHA npm test
 
scan:
  stage: scan
  image:
    name: aquasec/trivy
    entrypoint: [""]
  script:
    - trivy image --exit-code 1 --severity CRITICAL $IMAGE_NAME:$CI_COMMIT_SHA
  allow_failure: true
 
push:
  stage: push
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker load < image.tar
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker push $IMAGE_NAME:$CI_COMMIT_SHA
    - docker tag $IMAGE_NAME:$CI_COMMIT_SHA $IMAGE_NAME:latest
    - docker push $IMAGE_NAME:latest
  only:
    - main
 
deploy:
  stage: deploy
  image: alpine
  script:
    - apk add --no-cache curl
    - curl -X POST $DEPLOY_WEBHOOK_URL
  environment:
    name: production
  only:
    - main
  when: manual

GitLab with Kaniko

YAML
# .gitlab-ci.yml with Kaniko (no Docker daemon needed)
build:
  stage: build
  image:
    name: gcr.io/kaniko-project/executor:debug
    entrypoint: [""]
  script:
    - mkdir -p /kaniko/.docker
    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
    - /kaniko/executor
      --context $CI_PROJECT_DIR
      --dockerfile $CI_PROJECT_DIR/Dockerfile
      --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
      --destination $CI_REGISTRY_IMAGE:latest

CircleCI

YAML
# .circleci/config.yml
version: 2.1
 
orbs:
  docker: circleci/docker@2.4.0
 
jobs:
  build-and-test:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - setup_remote_docker:
          docker_layer_caching: true
      - docker/build:
          image: myapp
          tag: $CIRCLE_SHA1
      - run:
          name: Run tests
          command: |
            docker run --rm myapp:$CIRCLE_SHA1 npm test
 
  push:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - setup_remote_docker
      - docker/check
      - docker/build:
          image: $DOCKERHUB_USERNAME/myapp
          tag: $CIRCLE_SHA1,latest
      - docker/push:
          image: $DOCKERHUB_USERNAME/myapp
          tag: $CIRCLE_SHA1,latest
 
  deploy:
    docker:
      - image: cimg/base:stable
    steps:
      - run:
          name: Deploy
          command: |
            # Deployment commands
 
workflows:
  build-test-deploy:
    jobs:
      - build-and-test
      - push:
          requires:
            - build-and-test
          filters:
            branches:
              only: main
      - deploy:
          requires:
            - push
          filters:
            branches:
              only: main

Jenkins

Jenkinsfile

GROOVY
// Jenkinsfile
pipeline {
    agent any
 
    environment {
        DOCKER_IMAGE = 'myapp'
        REGISTRY = 'docker.io/myuser'
    }
 
    stages {
        stage('Build') {
            steps {
                script {
                    docker.build("${DOCKER_IMAGE}:${BUILD_NUMBER}")
                }
            }
        }
 
        stage('Test') {
            steps {
                script {
                    docker.image("${DOCKER_IMAGE}:${BUILD_NUMBER}").inside {
                        sh 'npm test'
                    }
                }
            }
        }
 
        stage('Security Scan') {
            steps {
                sh "trivy image --exit-code 0 --severity HIGH,CRITICAL ${DOCKER_IMAGE}:${BUILD_NUMBER}"
            }
        }
 
        stage('Push') {
            when {
                branch 'main'
            }
            steps {
                script {
                    docker.withRegistry('https://registry.hub.docker.com', 'docker-hub-credentials') {
                        docker.image("${DOCKER_IMAGE}:${BUILD_NUMBER}").push()
                        docker.image("${DOCKER_IMAGE}:${BUILD_NUMBER}").push('latest')
                    }
                }
            }
        }
 
        stage('Deploy') {
            when {
                branch 'main'
            }
            steps {
                sh './deploy.sh ${BUILD_NUMBER}'
            }
        }
    }
 
    post {
        always {
            cleanWs()
        }
    }
}

Build Optimization

Layer Caching

YAML
# GitHub Actions with cache
- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: myapp:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max
YAML
# GitLab with registry cache
build:
  script:
    - docker pull $CI_REGISTRY_IMAGE:latest || true
    - docker build
      --cache-from $CI_REGISTRY_IMAGE:latest
      -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

BuildKit Inline Cache

DOCKERFILE
# syntax=docker/dockerfile:1
 
FROM node:20-alpine AS base
WORKDIR /app
 
FROM base AS dependencies
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
 
FROM dependencies AS build
COPY . .
RUN npm run build
 
FROM base AS production
COPY --from=dependencies /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]
BASH
# Build with inline cache
docker build \
  --build-arg BUILDKIT_INLINE_CACHE=1 \
  -t myapp:latest .
 
# Use cache when building
docker build \
  --cache-from myapp:latest \
  -t myapp:new .

Testing in CI

Running Tests in Containers

YAML
# GitHub Actions
- name: Run unit tests
  run: |
    docker build --target test -t myapp:test .
    docker run --rm myapp:test npm test
 
- name: Run integration tests
  run: |
    docker compose -f docker-compose.test.yml up -d
    docker compose -f docker-compose.test.yml run test npm run test:integration
    docker compose -f docker-compose.test.yml down -v

Test Compose File

YAML
# docker-compose.test.yml
services:
  test:
    build:
      context: .
      target: test
    environment:
      - DATABASE_URL=postgres://test:test@postgres:5432/test
      - NODE_ENV=test
    depends_on:
      postgres:
        condition: service_healthy
 
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
      POSTGRES_DB: test
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U test"]
      interval: 2s
      timeout: 2s
      retries: 10
    tmpfs:
      - /var/lib/postgresql/data

Deployment Strategies

Deploy to Kubernetes

YAML
# GitHub Actions
- name: Deploy to Kubernetes
  run: |
    echo "${{ secrets.KUBECONFIG }}" > kubeconfig
    export KUBECONFIG=kubeconfig
    kubectl set image deployment/myapp \
      myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
    kubectl rollout status deployment/myapp

Deploy to Docker Swarm

YAML
- name: Deploy to Swarm
  uses: appleboy/ssh-action@master
  with:
    host: ${{ secrets.SWARM_HOST }}
    username: ${{ secrets.SWARM_USER }}
    key: ${{ secrets.SWARM_KEY }}
    script: |
      docker service update \
        --image myapp:${{ github.sha }} \
        myapp

Deploy to AWS ECS

YAML
- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v4
  with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    aws-region: us-east-1
 
- name: Login to Amazon ECR
  id: login-ecr
  uses: aws-actions/amazon-ecr-login@v2
 
- name: Build and push to ECR
  env:
    ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
    IMAGE_TAG: ${{ github.sha }}
  run: |
    docker build -t $ECR_REGISTRY/myapp:$IMAGE_TAG .
    docker push $ECR_REGISTRY/myapp:$IMAGE_TAG
 
- name: Update ECS service
  run: |
    aws ecs update-service \
      --cluster my-cluster \
      --service my-service \
      --force-new-deployment

Security in CI/CD

Secret Management

YAML
# Never hardcode secrets!
# Use CI/CD secret management
 
# GitHub Actions
env:
  DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
 
# GitLab CI
variables:
  DOCKER_TOKEN: $DOCKER_TOKEN # From CI/CD settings

Scanning in Pipeline

YAML
# GitHub Actions
- name: Scan for vulnerabilities
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: myapp:${{ github.sha }}
    format: "table"
    exit-code: "1"
    severity: "CRITICAL"
 
- name: Run Snyk
  uses: snyk/actions/docker@master
  with:
    image: myapp:${{ github.sha }}
    args: --severity-threshold=high

Warning

Always scan images before pushing to production registries. Fail the pipeline on critical vulnerabilities.

Quick Reference

GitHub Actions

ActionPurpose
docker/setup-buildx-actionSet up BuildKit
docker/login-actionRegistry authentication
docker/build-push-actionBuild and push images
docker/metadata-actionGenerate tags and labels
aquasecurity/trivy-actionVulnerability scanning

Build Caching

MethodDescription
type=ghaGitHub Actions cache
type=registryRegistry-based cache
type=localLocal directory cache
--cache-fromPull cache from image

Pipeline Best Practices

  • Use multi-stage builds for testing
  • Cache dependencies between builds
  • Scan for vulnerabilities before pushing
  • Use semantic versioning for tags
  • Require approval for production deploys
  • Keep secrets in CI/CD secret management
  • Run tests in containers for consistency
  • Build multi-platform images for broad compatibility

Congratulations! You've completed the Docker for Developers guide. You now have the knowledge to containerize applications, build efficient images, orchestrate multi-container applications, and integrate Docker into your development and deployment workflows.