Building a Docker image manually is a common starting point, but in any professional development environment, it’s a critical bottleneck. Manual builds introduce inconsistencies, slow down delivery, and are prone to human error. This chapter guides you through automating the core of Continuous Integration (CI) for Dockerized applications: consistently building your images and pushing them to a secure container registry.

By the end of this chapter, you will have implemented a robust CI pipeline using GitHub Actions. This pipeline will automatically build your Dockerized application, tag the image appropriately, and securely publish it to an Azure Container Registry (ACR) whenever code is pushed. This level of automation is foundational for reliable software delivery, ensuring that every deployable artifact is derived from your latest, validated codebase.

Project Overview: Automating Docker CI

This project focuses on establishing a Continuous Integration (CI) pipeline for a simple Dockerized Node.js application. The goal is to automate the entire process from code commit to a securely stored, versioned Docker image in a private container registry. This automation ensures consistency, reduces manual errors, and speeds up the delivery of new features or bug fixes.

Upon completion, you will have a working GitHub Actions workflow that:

  • Triggers automatically on code pushes to the main branch.
  • Checks out the latest code from your GitHub repository.
  • Securely authenticates with Azure Container Registry (ACR).
  • Builds a Docker image using a multi-stage Dockerfile.
  • Tags the image with both a latest tag and a unique Git commit SHA.
  • Pushes the tagged images to your private ACR instance.

Tech Stack Choices

To build this automated CI pipeline, we will leverage the following core technologies:

  • Docker Engine (via GitHub Actions Runner): The underlying technology for building and managing containers. GitHub Actions runners come pre-configured with a compatible Docker Engine. As of 2026-08-06, Docker Engine typically runs version 29.1.x or later on these runners.
  • Docker Compose: While not directly used within the CI build process itself in this chapter, Docker Compose (version 2.40.3 as of 2026-08-06) is a key tool for local multi-service development and is assumed for testing the application locally before CI.
  • GitHub Actions: The CI/CD platform native to GitHub. We use it to orchestrate the build, test, and push steps. Its declarative YAML syntax and extensive marketplace of actions simplify pipeline creation.
  • Azure Container Registry (ACR): A managed, private Docker image registry provided by Microsoft Azure. It offers robust security features, scalability, and integration with other Azure services, making it an excellent choice for production image storage.
  • Node.js & Express: Our sample application stack. The principles learned here are transferable to any language or framework.

Build Plan: Automating Image Builds

This chapter will guide you through the following incremental milestones:

  1. Prepare the Application & Dockerfile: Set up a basic Node.js Express application and create an efficient multi-stage Dockerfile.
  2. Provision Azure Container Registry (ACR): Create a dedicated, private registry instance in Azure to store your Docker images.
  3. Secure Azure Authentication: Create an Azure Service Principal with minimal necessary permissions for GitHub Actions to interact with ACR.
  4. Configure GitHub Secrets: Store sensitive ACR credentials securely within your GitHub repository.
  5. Define GitHub Actions Workflow: Create the YAML file that specifies the steps for building and pushing your Docker image.
  6. Execute and Verify: Trigger the CI pipeline and confirm that images are correctly built and pushed to ACR.

Architecting the CI Pipeline

Continuous Integration is a development practice where developers frequently integrate their code changes into a central repository. Each integration then triggers an automated build and test process. For containerized applications, this workflow typically involves several key steps that are orchestrated by the CI system.

Here’s a high-level architectural overview of the CI pipeline we will build:

flowchart TD A[Developer Push Code] --> B{GitHub Repository} B -->|Triggers CI| C[GitHub Actions] C --> D[Checkout Code] D --> E[Login ACR] E --> F[Build Image] F --> G[Push Image] G --> H[Image in Registry]

Why this specific architecture?

  • GitHub Actions: It’s natively integrated with GitHub repositories, simplifying workflow setup and management. GitHub Actions offers a vast marketplace of pre-built actions, accelerating pipeline development.
  • Azure Container Registry (ACR): A fully managed, secure, and scalable Docker image registry provided by Microsoft Azure. ACR is a best practice for production environments, offering robust access control, image scanning, and integration with other Azure services. Using a private registry significantly enhances security and control compared to relying solely on public alternatives.
  • Secure Credential Management: Sensitive information, such as cloud service credentials, will be handled securely using GitHub Secrets and Azure Service Principals. This prevents credentials from being exposed directly in your repository or workflow files, adhering to the principle of least privilege.

Step-by-Step Implementation

For this practical example, we’ll use a simple Node.js web application. We assume you’ve already created a GitHub repository for your project.

1. Project Setup and Dockerfile

First, ensure your project structure and Dockerfile are ready. Create a new directory, e.g., docker-ci-example, and set up the following files. This structure assumes your application code is within an app/ subdirectory.

app/index.js

const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('Hello from Dockerized CI! Version 1.0');
});

app.listen(port, () => {
  console.log(`App listening at http://localhost:${port}`);
});

This is a basic Express.js application that serves a “Hello World” message.

app/package.json

{
  "name": "docker-ci-example",
  "version": "1.0.0",
  "description": "A simple Node.js app for Docker CI demo",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^4.19.2"
  }
}

This defines the Node.js project and its express dependency.

Dockerfile (in the root of your repository)

# Stage 1: Build the application
# Use a lightweight Node.js base image for building
FROM node:20-alpine AS builder

WORKDIR /app

# Copy package.json and package-lock.json to install dependencies
COPY app/package*.json ./
RUN npm install

# Copy the rest of the application source code
COPY app/ .

# Stage 2: Create the final production image
# Use a fresh, minimal Node.js runtime image for production
FROM node:20-alpine

WORKDIR /app

# Copy only the necessary files from the builder stage
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/index.js .
COPY --from=builder /app/package.json .

# Expose the port the application listens on
EXPOSE 3000
# Define the command to run the application
CMD ["npm", "start"]

# 🔥 Optimization / Pro tip: Run as a non-root user
# For production deployments, always create and use a dedicated non-root user
# to mitigate potential security risks and adhere to the principle of least privilege.
# Example for a Node.js app:
# RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
# USER appuser
  • Decision: We employ a multi-stage Dockerfile. This is a crucial optimization technique. The first stage (builder) handles dependency installation, creating a temporary environment. The second stage then copies only the essential build artifacts (compiled code, node_modules, etc.) into a clean, minimal base image.
    • Tradeoff: This slightly increases the Dockerfile’s complexity but drastically reduces the final image size and attack surface, improving security and deployment speed.
  • node:20-alpine: This specifies Node.js version 20 on an Alpine Linux base. Alpine images are significantly smaller than their Debian-based counterparts, leading to faster downloads and smaller storage footprints.
  • Non-root User: The commented-out section highlights a critical security best practice. Running containers as a non-root user limits the potential damage if an attacker compromises the container.

2. Set up Azure Container Registry (ACR)

You will need an active Azure subscription for this step.

  1. Create an ACR instance:

    • Navigate to the Azure portal (portal.azure.com).
    • Search for “Container Registries” and select “Create”.
    • Fill in the required details:
      • Registry name: Choose a globally unique name (e.g., mydockerciacr2026).
      • Resource group: Select an existing one or create a new one.
      • Location: Choose a region close to your deployment target.
      • SKU: “Basic” is sufficient for this tutorial. For production, consider “Standard” or “Premium” for features like geo-replication and content trust.
    • Click “Review + create”, then “Create”.
    • 📌 Key Idea: A dedicated, private registry like ACR is essential for managing your images securely and reliably in production.
  2. Create a Service Principal (Recommended for Production): For production scenarios, using an Azure Service Principal with specific permissions is the most secure method for authenticating CI/CD pipelines.

    • Open Azure Cloud Shell or your local Azure CLI (ensure you’re logged in with az login).
    • Retrieve your ACR’s resource ID:
      ACR_NAME="mydockerciacr2026" # Replace with your ACR name
      ACR_ID=$(az acr show --name $ACR_NAME --query id --output tsv)
      echo $ACR_ID
    • Create a Service Principal and grant it the acrPush role to your ACR. This role allows it to push images but not delete or modify the registry configuration.
      SP_NAME="github-actions-sp-2026"
      SP_CREDENTIALS=$(az ad sp create-for-rbac --name $SP_NAME --scopes $ACR_ID --role acrPush --query "{clientId: appId, clientSecret: password, subscriptionId: subscription, tenantId: tenant}" --output json)
      echo $SP_CREDENTIALS
    • 🧠 Important: Save the entire JSON output from SP_CREDENTIALS. This JSON string contains your clientId (which acts as a username) and clientSecret (password), along with subscriptionId and tenantId. You will use this entire JSON string as a secret in GitHub.
    • ⚡ Quick Note: While ACR also offers an “Admin user” with username/password, it grants broad permissions and is less secure for automated pipelines. Service Principals offer granular control and are preferred.

3. Configure GitHub Secrets

To securely provide your ACR credentials to GitHub Actions, we’ll store them as encrypted secrets in your GitHub repository.

  1. Navigate to your GitHub repository in your web browser.

  2. Go to “Settings” > “Secrets and variables” > “Actions”.

  3. Click “New repository secret”.

  4. Create the following secrets:

    • ACR_LOGIN_SERVER: The login server for your ACR instance (e.g., mydockerciacr2026.azurecr.io). You can find this on the “Overview” page of your ACR in the Azure portal.
    • AZURE_CREDENTIALS: Paste the complete JSON string you obtained when creating the Service Principal (e.g., { "clientId": "...", "clientSecret": "...", "subscriptionId": "...", "tenantId": "..." }).

    ⚠️ What can go wrong: Ensure the JSON string for AZURE_CREDENTIALS is valid and copied completely without extra spaces or characters. Invalid JSON will cause authentication failures.

4. Create GitHub Actions Workflow

Now, let’s define the CI workflow in your repository. This YAML file will instruct GitHub Actions on how to build and push your Docker image.

  1. In the root of your repository, create a directory named .github/workflows.
  2. Inside this directory, create a new file named docker-ci.yml.

.github/workflows/docker-ci.yml

name: Docker CI for Web App

on:
  push:
    branches:
      - main # Trigger on pushes to the main branch
  pull_request:
    branches:
      - main # Trigger on pull requests targeting the main branch
  workflow_dispatch: # Allows manual triggering of the workflow from GitHub UI

env:
  REGISTRY: ${{ secrets.ACR_LOGIN_SERVER }} # Azure Container Registry login server
  IMAGE_NAME: docker-ci-example             # Name for your Docker image
  IMAGE_PATH: .                             # Context path for Dockerfile (repository root)

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest # Use the latest Ubuntu runner provided by GitHub Actions
    permissions:
      contents: read # Allow reading repository content
      id-token: write # Required for secure OIDC login to Azure

    steps:
    - name: Checkout repository code
      uses: actions/[email protected] # As of 2026-08-06, checks out the repository

    - name: Set up Docker Buildx
      uses: docker/[email protected] # As of 2026-08-06, enables advanced Docker build features

    - name: Log in to Azure Container Registry
      uses: azure/[email protected] # As of 2026-08-06, securely logs in to Azure
      with:
        creds: ${{ secrets.AZURE_CREDENTIALS }} # Use the Service Principal credentials from GitHub Secrets
        enable-AzPSSession: false # Not strictly needed for ACR login, set to false for minimal permissions

    - name: Build and push Docker image
      uses: docker/[email protected] # As of 2026-08-06, builds and pushes the Docker image
      with:
        context: ${{ env.IMAGE_PATH }} # Set the build context to the repository root
        push: true                      # Enable pushing the image to the registry
        tags: |
          ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} # Tag with full Git commit SHA for immutability
          ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest            # Tag as 'latest' for convenience
        cache-from: type=gha            # Enable caching from GitHub Actions cache
        cache-to: type=gha,mode=max     # Store build cache to GitHub Actions cache (max mode for aggressive caching)
  • name: A human-readable name for your workflow, visible in the GitHub Actions UI.
  • on: Defines the events that trigger this workflow. Here, it runs on push or pull_request to the main branch, or it can be triggered manually via workflow_dispatch.
  • env: Sets environment variables available to all jobs in the workflow.
    • REGISTRY: Dynamically retrieves the ACR login server from GitHub Secrets.
    • IMAGE_NAME: The chosen name for your Docker image within the registry.
    • IMAGE_PATH: Crucially, set to . (the repository root). This ensures that the Docker build context includes both the Dockerfile and the app/ directory, allowing COPY app/ commands in the Dockerfile to function correctly.
  • jobs: Workflows are composed of one or more jobs.
    • build-and-push-image: Our single job for this pipeline.
      • runs-on: ubuntu-latest: Specifies the virtual machine environment where the job will execute.
      • permissions: This block is vital for security. id-token: write enables OpenID Connect (OIDC) authentication with Azure, a more secure, token-based approach that avoids long-lived secrets when possible.
      • actions/[email protected]: This action clones your repository’s code onto the runner.
      • docker/[email protected]: Initializes Docker Buildx, which provides enhanced build capabilities, including multi-platform builds and improved layer caching.
      • azure/[email protected]: Logs into Azure using the AZURE_CREDENTIALS secret (your Service Principal JSON). This step authenticates the runner to interact with Azure services, including ACR.
      • docker/[email protected]: This powerful action handles both building your Docker image and pushing it to the specified registry.
        • context: ${{ env.IMAGE_PATH }}: Specifies the root directory for the Docker build.
        • push: true: Instructs the action to push the built image.
        • tags: Defines the tags for the image. We’re applying two tags: one with the unique github.sha (the full Git commit hash) for traceability, and a latest tag for convenience during development.
        • cache-from, cache-to: These leverage GitHub Actions’ built-in cache. Docker layers are cached between workflow runs, significantly accelerating subsequent builds by reusing unchanged layers.

Important Version Notes (checked 2026-08-06):

  • actions/checkout: Latest stable is v4.1.7.
  • docker/setup-buildx-action: Latest stable is v3.3.0.
  • azure/login: Latest stable is v1.6.1.
  • docker/build-push-action: Latest stable is v5.3.0. Always consult the GitHub Marketplace or official documentation for the absolute latest stable versions of these actions, as they are frequently updated.

Testing & Verification

Once your workflow file is committed, it’s time to see the automation in action.

  1. Commit and Push: Save all your changes (app/, Dockerfile, and .github/workflows/docker-ci.yml) and push them to your main branch on GitHub.

    git add .
    git commit -m "feat: Implement Docker CI pipeline with GitHub Actions"
    git push origin main
  2. Monitor GitHub Actions:

    • Go to your GitHub repository in your browser.
    • Click on the “Actions” tab.
    • You should see a new workflow run initiated by your push (it will have the commit message as its title). Click on this run.
    • Observe the progress of each step: Checkout repository code, Set up Docker Buildx, Log in to Azure Container Registry, and Build and push Docker image.
    • Each step should eventually show a green checkmark, indicating success. If any step fails, click on it to expand the logs and review the error messages for debugging clues.
  3. Verify Image in ACR:

    • Once the GitHub Actions workflow successfully completes, navigate back to your Azure Container Registry in the Azure portal.
    • In your ACR instance, go to “Repositories” under the “Services” section.
    • You should now see a new repository named docker-ci-example. Click on it.
    • Inside, you will find your newly pushed images, tagged with both the latest tag and the full Git commit SHA (e.g., abcdef123).

    Alternatively, you can use the Azure CLI to verify:

    # Ensure you are logged in to Azure CLI
    az login
    
    # List repositories in your ACR
    az acr repository list --name <your-acr-name> --output tsv
    
    # Show tags for your specific image
    az acr repository show-tags --name <your-acr-name> --repository docker-ci-example --output tsv

    You should see output similar to latest and a long string representing the Git commit SHA.

Production Considerations

Implementing CI for Dockerized applications introduces several critical considerations for production environments:

  • Secure Credential Handling: The use of GitHub Secrets and Azure Service Principals with specific roles (like acrPush) is paramount. Never embed credentials directly in your workflow files. The id-token: write permission for OIDC-based authentication is the modern, secure approach to avoid static secrets where possible.
  • Image Versioning Strategy: While latest is convenient for development, in production, always rely on immutable tags like the Git commit SHA (github.sha) or semantic versioning (e.g., v1.0.0, v1.0.1). This ensures that deployments are always tied to a specific, reproducible codebase.
    • ⚡ Real-world insight: Many teams use latest for development/staging and then promote specific SHA-tagged images or manually apply semantic version tags for production releases.
  • Automated Image Scanning: Azure Container Registry offers integrated vulnerability scanning through Microsoft Defender for Cloud. Enable this feature to automatically scan all new images for known security vulnerabilities upon push. This is a critical line of defense before deployment.
  • Multi-Platform Builds: Docker Buildx, enabled by docker/setup-buildx-action, allows you to build images for multiple architectures (e.g., linux/amd64, linux/arm64) within a single workflow. This is crucial for supporting diverse deployment targets, from cloud VMs to edge devices.
  • Efficient Build Caching: The cache-from and cache-to parameters in docker/build-push-action are vital for reducing CI build times and costs. By caching Docker layers, only changed layers need to be rebuilt, leading to significant performance improvements for frequent pushes.
  • CI Runner Resource Management: Be mindful of the resource limits provided by your CI service (e.g., GitHub-hosted runners). For very large or complex Docker builds, you might need to consider self-hosted runners with more powerful hardware to avoid timeouts or performance bottlenecks.
  • Container Health Checks: While not directly part of the build pipeline, ensure your application’s Dockerfile or orchestration configuration includes health checks (e.g., HEALTHCHECK instruction in Dockerfile, readiness/liveness probes in Kubernetes). This ensures that deployed containers are truly ready to serve traffic.

Common Issues and Troubleshooting

Even with a well-designed pipeline, issues can arise. Here are some common problems and their debugging strategies:

  1. azure/login failure: “Permissions for the credential could not be verified.”

    • Issue: The AZURE_CREDENTIALS secret is incorrect, incomplete, or the associated Service Principal lacks the necessary permissions (e.g., acrPush role on the ACR).
    • Solution: Carefully re-verify the AZURE_CREDENTIALS JSON string in your GitHub Secret for any typos or missing fields (clientId, clientSecret, tenantId, subscriptionId). Ensure the Service Principal has been granted the acrPush role on your specific ACR instance. If using OIDC (id-token: write), confirm the permissions block is correctly configured in your workflow.
  2. docker/build-push-action failure: “denied: requested access to the resource is denied”

    • Issue: The authenticated identity (Service Principal) does not have permission to push images to the specified Azure Container Registry.
    • Solution: Confirm that the Service Principal used in AZURE_CREDENTIALS has the acrPush role assigned to your ACR. Also, double-check that the REGISTRY environment variable in your workflow (${{ secrets.ACR_LOGIN_SERVER }}) correctly points to your ACR’s login server.
  3. Image build failure: “Cannot find module ’express’” or similar application errors.

    • Issue: The Dockerfile or the application code itself has an error, or dependencies were not correctly installed or copied during the build process.
    • Solution: Review the build logs in GitHub Actions carefully. Check your Dockerfile for correct paths, COPY commands, and ensure npm install (or equivalent for your language) runs successfully. Verify that the context parameter in docker/build-push-action (${{ env.IMAGE_PATH }}) is correctly set to the directory containing your Dockerfile and source code.
  4. Incorrect Image Tagging or Image Not Appearing in ACR.

    • Issue: The image is built but doesn’t appear in ACR with the expected tags, or it’s missing entirely.
    • Solution: Inspect the tags parameter within your docker/build-push-action in the workflow file. Verify that the environment variables (${{ env.REGISTRY }}, ${{ env.IMAGE_NAME }}, ${{ github.sha }}) are resolving to the correct values by checking the workflow run logs. Ensure push: true is set.

Summary & Next Steps

You’ve successfully implemented a Continuous Integration pipeline for your Dockerized application using GitHub Actions and Azure Container Registry. This is a foundational achievement in modern software development, transitioning from manual, error-prone builds to an automated, consistent process.

With this setup, you can now:

  • Automatically build Docker images for your application on every code commit or pull request.
  • Securely store these versioned images in a private, managed container registry.
  • Ensure a high level of consistency and reliability in your build artifacts.

This automated build and registry push lays the groundwork for Continuous Delivery (CD). The natural progression from here is to automate the deployment of these newly built and pushed images to your staging, testing, or production environments. This next phase typically involves integrating with orchestration platforms like Kubernetes, Azure Container Apps, or other cloud-native deployment services that consume images from your ACR. This is where your journey into full CI/CD truly begins, transforming your development process into a streamlined, efficient, and robust system.

References

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.