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
mainbranch. - 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
latesttag 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:
- Prepare the Application & Dockerfile: Set up a basic Node.js Express application and create an efficient multi-stage
Dockerfile. - Provision Azure Container Registry (ACR): Create a dedicated, private registry instance in Azure to store your Docker images.
- Secure Azure Authentication: Create an Azure Service Principal with minimal necessary permissions for GitHub Actions to interact with ACR.
- Configure GitHub Secrets: Store sensitive ACR credentials securely within your GitHub repository.
- Define GitHub Actions Workflow: Create the YAML file that specifies the steps for building and pushing your Docker image.
- 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:
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.
- Tradeoff: This slightly increases the
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.
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.
- Registry name: Choose a globally unique name (e.g.,
- Click “Review + create”, then “Create”.
- 📌 Key Idea: A dedicated, private registry like ACR is essential for managing your images securely and reliably in production.
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
acrPushrole 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 yourclientId(which acts as a username) andclientSecret(password), along withsubscriptionIdandtenantId. 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.
- Open Azure Cloud Shell or your local Azure CLI (ensure you’re logged in with
3. Configure GitHub Secrets
To securely provide your ACR credentials to GitHub Actions, we’ll store them as encrypted secrets in your GitHub repository.
Navigate to your GitHub repository in your web browser.
Go to “Settings” > “Secrets and variables” > “Actions”.
Click “New repository secret”.
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_CREDENTIALSis 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.
- In the root of your repository, create a directory named
.github/workflows. - 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 onpushorpull_requestto themainbranch, or it can be triggered manually viaworkflow_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 theDockerfileand theapp/directory, allowingCOPY app/commands in theDockerfileto 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: writeenables 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 theAZURE_CREDENTIALSsecret (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 uniquegithub.sha(the full Git commit hash) for traceability, and alatesttag 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 isv4.1.7.docker/setup-buildx-action: Latest stable isv3.3.0.azure/login: Latest stable isv1.6.1.docker/build-push-action: Latest stable isv5.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.
Commit and Push: Save all your changes (
app/,Dockerfile, and.github/workflows/docker-ci.yml) and push them to yourmainbranch on GitHub.git add . git commit -m "feat: Implement Docker CI pipeline with GitHub Actions" git push origin mainMonitor 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, andBuild 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.
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
latesttag 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 tsvYou should see output similar to
latestand 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. Theid-token: writepermission for OIDC-based authentication is the modern, secure approach to avoid static secrets where possible. - Image Versioning Strategy: While
latestis 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
latestfor development/staging and then promote specific SHA-tagged images or manually apply semantic version tags for production releases.
- ⚡ Real-world insight: Many teams use
- 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-fromandcache-toparameters indocker/build-push-actionare 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
Dockerfileor orchestration configuration includes health checks (e.g.,HEALTHCHECKinstruction 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:
azure/loginfailure: “Permissions for the credential could not be verified.”- Issue: The
AZURE_CREDENTIALSsecret is incorrect, incomplete, or the associated Service Principal lacks the necessary permissions (e.g.,acrPushrole on the ACR). - Solution: Carefully re-verify the
AZURE_CREDENTIALSJSON string in your GitHub Secret for any typos or missing fields (clientId,clientSecret,tenantId,subscriptionId). Ensure the Service Principal has been granted theacrPushrole on your specific ACR instance. If using OIDC (id-token: write), confirm thepermissionsblock is correctly configured in your workflow.
- Issue: The
docker/build-push-actionfailure: “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_CREDENTIALShas theacrPushrole assigned to your ACR. Also, double-check that theREGISTRYenvironment variable in your workflow (${{ secrets.ACR_LOGIN_SERVER }}) correctly points to your ACR’s login server.
Image build failure: “Cannot find module ’express’” or similar application errors.
- Issue: The
Dockerfileor 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
Dockerfilefor correct paths,COPYcommands, and ensurenpm install(or equivalent for your language) runs successfully. Verify that thecontextparameter indocker/build-push-action(${{ env.IMAGE_PATH }}) is correctly set to the directory containing yourDockerfileand source code.
- Issue: The
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
tagsparameter within yourdocker/build-push-actionin 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. Ensurepush: trueis 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
- GitHub Actions Documentation
- Azure Container Registry Documentation
- Best Practices for Using Azure Container Registry - Azure Container Registry | Microsoft Learn
- docker/build-push-action GitHub Marketplace
- azure/login GitHub Marketplace
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.