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.
🚀 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.
🔧 GitHub Actions — Full Docker Workflow
# .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
# 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.
# 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
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.
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.
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
- CI/CD pipelines are the standard way to build and push Docker images — never build manually in deployments.
- Build once, promote the same image across environments — never rebuild between dev/staging/prod.
- Tag with git SHAs + versions for full traceability.
- Use GitHub Actions
docker/build-push-actionor Azure DevOpsDocker@2task for standardized workflows. - Use layer cache in CI to cut build times dramatically.