Introduction to Azure Pipelines with Docker

This guide is a community contribution. Docker would like to thank Kristiyan Velkov for his valuable contribution.

Prerequisites

Before you begin, ensure you have the following requirements:

Overview

This guide walks you through building and pushing Docker images using Azure Pipelines, enabling a streamlined and secure CI workflow for containerized applications. You’ll learn how to:

  • Configure Docker authentication securely.
  • Set up an automated pipeline to build and push images.

Set up Azure DevOps to work with Docker Hub

Step 1: Configure a Docker Hub service connection

To securely authenticate with Docker Hub using Azure Pipelines:

  1. Navigate to Project Settings > Service Connections in your Azure DevOps project.
  2. Select New service connection > Docker Registry.
  3. Choose Docker Hub and provide your Docker Hub credentials or access token.
  4. Give the service connection a recognizable name, such as my-docker-registry.
  5. Grant access only to the specific pipeline(s) that require it for improved security and least privilege.
Important

Avoid selecting the option to grant access to all pipelines unless absolutely necessary. Always apply the principle of least privilege.

Step 2: Create your pipeline

Add the following azure-pipelines.yml file to the root of your repository:

# Trigger pipeline on commits to the main branch
trigger:
  - main

# Trigger pipeline on pull requests targeting the main branch
pr:
  - main

# Define variables for reuse across the pipeline
variables:
  imageName: 'docker.io/$(dockerUsername)/my-image'
  buildTag: '$(Build.BuildId)'
  latestTag: 'latest'

stages:
  - stage: BuildAndPush
    displayName: Build and Push Docker Image
    jobs:
      - job: DockerJob
        displayName: Build and Push
        pool:
          vmImage: ubuntu-latest
          demands:
            - docker
        steps:
          - checkout: self
            displayName: Checkout Code

          - task: Docker@2
            displayName: Docker Login
            inputs:
              command: login
              containerRegistry: 'my-docker-registry'  # Service connection name

          - task: Docker@2
            displayName: Build Docker Image
            inputs:
              command: build
              repository: $(imageName)
              tags: |
                $(buildTag)
                $(latestTag)
              dockerfile: './Dockerfile'
              arguments: |
                --sbom=true
                --attest type=provenance
                --cache-from $(imageName):latest
            env:
              DOCKER_BUILDKIT: 1

          - task: Docker@2
            displayName: Push Docker Image
            condition: eq(variables['Build.SourceBranch'], 'refs/heads/main')
            inputs:
              command: push
              repository: $(imageName)
              tags: |
                $(buildTag)
                $(latestTag)

          # Optional: logout for self-hosted agents
          - script: docker logout
            displayName: Docker Logout (Self-hosted only)
            condition: ne(variables['Agent.OS'], 'Windows_NT')

What this pipeline does

This pipeline automates the Docker image build and deployment process for the main branch. It ensures a secure and efficient workflow with best practices like caching, tagging, and conditional cleanup. Here's what it does:

  • Triggers on commits and pull requests targeting the main branch.
  • Authenticates securely with Docker Hub using an Azure DevOps service connection.
  • Builds and tags the Docker image using Docker BuildKit for caching.
  • Pushes both buildId and latest tags to Docker Hub.
  • Logs out from Docker if running on a self-hosted Linux agent.

How the pipeline works

Step 1: Define pipeline triggers

trigger:
  - main

pr:
  - main

This pipeline is triggered automatically on:

  • Commits pushed to the main branch
  • Pull requests targeting main main branch
Tip

Step 2: Define common variables

variables:
  imageName: 'docker.io/$(dockerUsername)/my-image'
  buildTag: '$(Build.BuildId)'
  latestTag: 'latest'

These variables ensure consistent naming, versioning, and reuse throughout the pipeline steps:

  • imageName: your image path on Docker Hub
  • buildTag: a unique tag for each pipeline run
  • latestTag: a stable alias for your most recent image
Important

The variable dockerUsername is not set automatically.
Set it securely in your Azure DevOps pipeline variables:

  1. Go to Pipelines > Edit > Variables
  2. Add dockerUsername with your Docker Hub username

Learn more: Define and use variables in Azure Pipelines

Step 3: Define pipeline stages and jobs

stages:
  - stage: BuildAndPush
    displayName: Build and Push Docker Image

This stage executes only if the source branch is main.

Tip

Step 4: Job configuration

jobs:
  - job: DockerJob
  displayName: Build and Push
  pool:
    vmImage: ubuntu-latest
    demands:
      - docker

This job utilizes the latest Ubuntu VM image with Docker support, provided by Microsoft-hosted agents. It can be replaced with a custom pool for self-hosted agents if necessary.

Tip

Step 4.1: Checkout code

steps:
  - checkout: self
    displayName: Checkout Code

This step pulls your repository code into the build agent, so the pipeline can access the Dockerfile and application files.

Tip

Step 4.2: Authenticate to Docker Hub

- task: Docker@2
  displayName: Docker Login
  inputs:
    command: login
    containerRegistry: 'my-docker-registry'  # Replace with your service connection name

Uses a pre-configured Azure DevOps Docker registry service connection to authenticate securely without exposing credentials directly.

Tip

Step 4.3: Build the Docker image

 - task: Docker@2
    displayName: Build Docker Image
    inputs:
      command: build
      repository: $(imageName)
      tags: |
          $(buildTag)
          $(latestTag)
      dockerfile: './Dockerfile'
      arguments: |
          --sbom=true
          --attest type=provenance
          --cache-from $(imageName):latest
    env:
      DOCKER_BUILDKIT: 1

This builds the image with:

  • Two tags: one with the unique Build ID and one as latest
  • Docker BuildKit enabled for faster builds and efficient layer caching
  • Cache pull from the most recent pushed latest image
  • Software Bill of Materials (SBOM) for supply chain transparency
  • Provenance attestation to verify how and where the image was built
Tip

Step 4.4: Push the Docker image

- task: Docker@2
  displayName: Push Docker Image
  condition: eq(variables['Build.SourceBranch'], 'refs/heads/main')
  inputs:
      command: push
      repository: $(imageName)
      tags: |
        $(buildTag)
        $(latestTag)

By applying this condition, the pipeline builds the Docker image on every run to ensure early detection of issues, but only pushes the image to the registry when changes are merged into the main branch—keeping your Docker Hub clean and focused

This uploads both tags to Docker Hub:

  • $(buildTag) ensures traceability per run.
  • latest is used for most recent image references.

Step 4.5 Logout of Docker (self-hosted agents)

- script: docker logout
  displayName: Docker Logout (Self-hosted only)
  condition: ne(variables['Agent.OS'], 'Windows_NT')

Executes docker logout at the end of the pipeline on Linux-based self-hosted agents to proactively clean up credentials and enhance security posture.

Summary

With this Azure Pipelines CI setup, you get:

  • Secure Docker authentication using a built-in service connection.
  • Automated image building and tagging triggered by code changes.
  • Efficient builds leveraging Docker BuildKit cache.
  • Safe cleanup with logout on persistent agents.
  • Build images that meet modern software supply chain requirements with SBOM and attestation

Learn more