CI/CD Integration
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:
┌─────────────────────────────────────────────────────────┐
│ CI/CD Pipeline │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Build │─▶│ Test │─▶│ Push │─▶│ Deploy │ │
│ │ Image │ │ Image │ │ Image │ │ │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ Dockerfile Tests in Registry Production │
│ + Context Container (Hub/ECR) Environment │
└─────────────────────────────────────────────────────────┘Pipeline Stages
| Stage | Purpose | Outcome |
|---|---|---|
| Build | Create image from Dockerfile | Image artifact |
| Test | Run tests inside container | Pass/fail status |
| Scan | Check for vulnerabilities | Security report |
| Push | Upload to registry | Stored image |
| Deploy | Update running services | New version live |
GitHub Actions
Basic Build and Push
# .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=maxMulti-Platform Build
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:latestComplete CI Pipeline
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
# .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: manualGitLab with Kaniko
# .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:latestCircleCI
# .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: mainJenkins
Jenkinsfile
// 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
# 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# 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_SHABuildKit Inline Cache
# 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"]# 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
# 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 -vTest Compose File
# 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/dataDeployment Strategies
Deploy to Kubernetes
# 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/myappDeploy to Docker Swarm
- 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 }} \
myappDeploy to AWS ECS
- 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-deploymentSecurity in CI/CD
Secret Management
# 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 settingsScanning in Pipeline
# 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=highWarning
Always scan images before pushing to production registries. Fail the pipeline on critical vulnerabilities.
Quick Reference
GitHub Actions
| Action | Purpose |
|---|---|
docker/setup-buildx-action | Set up BuildKit |
docker/login-action | Registry authentication |
docker/build-push-action | Build and push images |
docker/metadata-action | Generate tags and labels |
aquasecurity/trivy-action | Vulnerability scanning |
Build Caching
| Method | Description |
|---|---|
type=gha | GitHub Actions cache |
type=registry | Registry-based cache |
type=local | Local directory cache |
--cache-from | Pull 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.