AdvancedLesson 9 of 16

🚀 Docker in CI/CD with Azure DevOps and GitHub Actions

Automate the full image lifecycle — build, test, scan, tag, and push to ACR — using Azure DevOps Pipelines and GitHub Actions workflows.

🧒 Simple Explanation (ELI5)

Without CI/CD, every developer builds and pushes their own images manually — dangerous and inconsistent. With CI/CD, every code push automatically triggers a robot that builds a fresh, tested, and properly tagged image and puts it in the registry, ready to deploy. You never run docker build in production manually.

💡
Golden rule for CI/CD with Docker

Build once, promote everywhere. The same image built in CI is promoted through dev → staging → prod. Never rebuild between environments — rebuilding introduces the risk of non-deterministic differences.

🔧 GitHub Actions — Full Docker Workflow

yaml
# .github/workflows/docker.yml
name: Build and Push Docker Image

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: myacr.azurecr.io
  IMAGE_NAME: myapp

jobs:
  build-and-push:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Log in to Azure Container Registry
        uses: azure/docker-login@v1
        with:
          login-server: ${{ env.REGISTRY }}
          username: ${{ secrets.ACR_USERNAME }}
          password: ${{ secrets.ACR_PASSWORD }}

      - name: Extract metadata (tags, labels)
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=git-
            type=semver,pattern={{version}}
            type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

🔧 Azure DevOps Pipeline

yaml
# azure-pipelines.yml
trigger:
  branches:
    include: [main]

variables:
  acrLoginServer: myacr.azurecr.io
  imageName: myapp
  tag: $(Build.BuildId)

pool:
  vmImage: ubuntu-latest

stages:
- stage: Build
  jobs:
  - job: BuildAndPush
    steps:
    - task: AzureCLI@2
      displayName: Login to ACR
      inputs:
        azureSubscription: MyAzureConnection
        scriptType: bash
        scriptLocation: inlineScript
        inlineScript: az acr login --name myacr

    - task: Docker@2
      displayName: Build and Push
      inputs:
        command: buildAndPush
        repository: $(imageName)
        containerRegistry: MyACRServiceConnection
        tags: |
          $(tag)
          latest

🐛 Debugging Scenario

Problem: CI builds succeed but the pushed image is 800MB — too large, slow to pull.

bash
# Step 1: analyze the image layers locally docker image history myapp:latest --no-trunc | head -20 # Step 2: use dive tool for visual layer analysis docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock \ wagoodman/dive myapp:latest # Common causes of bloat: # - node_modules or build tools left in final image -> use multi-stage build # - COPY . . before .dockerignore is properly set up # - Multiple RUN commands not chained (each creates a layer) # - Debug packages installed but not removed # Fix: switch to multi-stage build (covered in Lesson 11)

🎯 Interview Questions

What is the recommended tagging strategy for CI/CD pipelines?

Tag with multiple labels simultaneously: 1) git SHA (git-a1b2c3d) for immutable traceability. 2) Semantic version (1.2.3) when a release is cut. 3) Environment tag (staging, prod) for promotion tracking. Never use only :latest — it is not traceable. The docker/metadata-action in GitHub Actions automates this correctly.

How do you securely authenticate to ACR in a CI/CD pipeline?

Best practice for Azure: use a Managed Identity (for Azure-hosted runners) or a service principal with minimal ACR push permissions (AcrPush role). Store credentials as pipeline secrets — never hardcode them. In GitHub Actions use GITHUB_SECRETS; in Azure DevOps use variable groups with key vault integration. Rotate service principal credentials regularly.

Scenario: Your CI spends 8 minutes building the Docker image on every run. How do you speed it up?

1. Order Dockerfile layers correctly (deps before code) for cache hits. 2. Use GitHub Actions cache for layer storage: cache-from/to: type=gha. 3. Use Docker BuildKit for parallel execution. 4. Use registry cache: build with --cache-from registry pulling previous layers. 5. Use multi-stage builds to avoid unnecessary work. 6. Pin base image versions to avoid unnecessary re-pulls.

📋 Summary