Questions
Design a CI/CD pipeline for building, testing, and deploying Docker images with proper tagging strategy.
The Scenario
Your team needs to set up a CI/CD pipeline for a containerized application. Current problems:
- Developers use
docker buildlocally with inconsistent results - Images are tagged as
latest, making rollbacks impossible - No automated testing before deployment
- No vulnerability scanning
- Manual deployments to production
- No way to trace deployed images back to source commits
The Challenge
Design a complete CI/CD pipeline that builds, tests, scans, and deploys Docker images with a proper tagging strategy. Explain how to ensure reproducible builds and enable reliable rollbacks.
A junior engineer might tag everything as latest, skip testing because 'it works locally', build directly on the production server, store credentials in the Dockerfile, and deploy by SSH-ing into servers. These lead to untraceable deployments, broken production from untested code, security vulnerabilities, inconsistent builds, and no rollback capability.
A senior engineer implements a complete pipeline: semantic versioning with git SHA tags, multi-stage builds for reproducibility, automated testing before image creation, vulnerability scanning before pushing, immutable image tags, signed images for verification, and GitOps for deployment. Every deployed image is traceable to a specific commit and can be rolled back instantly.
Step 1: Image Tagging Strategy
# BAD: Mutable tags
myapp:latest # Which version? Nobody knows
myapp:dev # Changes constantly
myapp:stable # When was this built?
# GOOD: Immutable, traceable tags
myapp:1.2.3 # Semantic version
myapp:1.2.3-abc1234 # Version + short SHA
myapp:sha-abc1234def5678 # Full commit SHA
myapp:pr-123 # Pull request preview
myapp:1.2.3-abc1234-20240115-142030 # Version + SHA + timestampStep 2: Complete GitHub Actions Pipeline
name: Build, Test, and Deploy
on:
push:
branches: [main, develop]
tags: ['v*']
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# ============================================
# Build and Test
# ============================================
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
security-events: write
outputs:
image_tags: ${{ steps.meta.outputs.tags }}
image_digest: ${{ steps.build.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
# Branch name
type=ref,event=branch
# PR number
type=ref,event=pr
# Semantic version from tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
# Git SHA
type=sha,prefix=sha-
# Latest for main branch
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push
id: build
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILD_DATE=${{ github.event.head_commit.timestamp }}
VCS_REF=${{ github.sha }}
VERSION=${{ steps.meta.outputs.version }}
# ============================================
# Security Scan
# ============================================
scan:
needs: build
runs-on: ubuntu-latest
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 scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
- name: Fail on critical vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
format: 'table'
exit-code: '1'
severity: 'CRITICAL'
# ============================================
# Integration Tests
# ============================================
test:
needs: build
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: test
ports:
- 5432:5432
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run integration tests
run: |
docker run --network host \
-e DATABASE_URL=postgres://postgres:test@localhost:5432/test \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} \
npm run test:integration
# ============================================
# Deploy to Staging
# ============================================
deploy-staging:
needs: [build, scan, test]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: staging
steps:
- name: Deploy to Kubernetes
uses: azure/k8s-deploy@v4
with:
manifests: k8s/staging/
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
# ============================================
# Deploy to Production
# ============================================
deploy-production:
needs: deploy-staging
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy to Kubernetes
uses: azure/k8s-deploy@v4
with:
manifests: k8s/production/
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}Step 3: Dockerfile with Build Metadata
# syntax=docker/dockerfile:1.4
FROM node:18-alpine AS builder
ARG BUILD_DATE
ARG VCS_REF
ARG VERSION
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS production
# OCI Image Labels (standard metadata)
LABEL org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.revision="${VCS_REF}" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.source="https://github.com/myorg/myapp"
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
CMD ["node", "dist/server.js"] Tagging Strategy Summary
| Trigger | Tag Pattern | Use Case |
|---|---|---|
| Push to main | sha-abc1234, latest | Development builds |
| Pull request | pr-123 | Preview environments |
| Git tag v1.2.3 | 1.2.3, 1.2, 1 | Production releases |
| Scheduled | nightly-20240115 | Nightly builds |
Rollback Strategy
# List available versions
docker images myapp --format "{{.Tag}}" | head -10
# Rollback to specific version
kubectl set image deployment/api api=myapp:1.2.2
# Or using Helm
helm rollback api-release 3
Image Signing (Cosign)
- name: Sign image with Cosign
uses: sigstore/cosign-installer@v3
- name: Sign the image
run: |
cosign sign --yes \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
Practice Question
Why is using only the 'latest' tag for Docker images problematic in production?