Blog Post

Build a C++ Pipeline with Docker, GitHub Actions, Azure ACR and Azure App Service

In today’s Software Development Life Cycle (SDLC), having a robust build pipeline is very important for ensuring that there is a smooth deployment and maintainable code quality. Building a C++ project can be challenging, often due to dependency issues, platform-specific issues, and inconsistent build environments. Leveraging Docker along with a CI/CD tool like GitHub Actions provides an effective solution by ensuring a consistent build environment and seamless packaging of dependencies.

Developing modern C++ projects requires consistent build environments, reliable testing, and automated deployment processes. You will be creating the pipeline to handle the following:

  • Build C++ applications in an isolated environment using containers
  • Running automated tests
  • Create a Docker image and deploy it to Azure Container Registry (ACR)
  • Automating the deployment process with GitHub Actions
  • Deploy the Docker Container on Azure App Service

Azure App Service is a fully managed Platform as a Service (PaaS) offered from Microsoft Azure which lets you to build, host and scale web apps, RESTful APIs and mobile backends in different programming languages and a custom Docker containers.

Why use Docker for C++ Builds

Building a C++ application often requires managing libraries, compilers, and other dependencies that might vary between different development and production environments. Docker solves this problem by providing a containerized environment that ensures consistency across builds.

Benefits of Docker for C++ Projects:

  • Consistency: Build the same way on every machine.
  • Isolation: Avoid conflict between different dependencies.
  • Portability: You can run your build anywhere Docker is supported.
  • Ease of Use: Simplify setup for developers.

In this blog post, we will walk you through creating a build pipeline for C++ using Docker and integrating it with the CICD solution (GitHub Actions) and finally deploying to Azure App Service.

Get Started

The following are the prerequisite

  • Basic knowledge of C++ and CMake.
  • Docker installed on your system.
  • Access to a Git repository
  • GitHub account for running GitHub Actions (CICD)

Step 1: Setting Up the Project

Let’s start with a simple C++ project to demonstrate the functionality of the pipeline.

a. Create a folder with the folder name src and a file inside that folder called main.cpp . Copy and paste the code in the file. The program calculates the average of a list of numbers.

https://medium.com/media/255dfb7990751a68129ee81cb068f8bc/href

b. Create a file with a name CMakeLists.txt on the root project. CMakeLists.txt file will contain configuration details for building your C++ application. This also includes configuration for running the test case.

https://medium.com/media/5908ecf842127796b605749c23676068/href

Step 2: Create a Dockerfile for your C++ Project

a. Create a file with a filename Dockerfile in your root project. This will be used to build the C++ project.

FROM ubuntu:25.04

RUN apt-get update && apt-get install -y
build-essential
cmake
git
libgtest-dev
libboost-system-dev
libboost-date-time-dev
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Clone Crow and Asio
RUN git clone https://github.com/CrowCpp/Crow.git /app/crow
RUN git clone https://github.com/chriskohlhoff/asio.git /app/asio

# Copy project code
COPY . .

RUN mkdir -p build
WORKDIR /app/build

# Build both app and test
RUN cmake .. && make -j$(nproc)

# You can also build your test binary alongside your main binary
EXPOSE 8080

CMD ["./cpp_project"]

b. Create a .dockerignore file and add this code snippet. This will speed up the build process and reduce the image size.

https://medium.com/media/284e9447c3e9edd11662a11fd450cdc8/href

Step 3: Build and Test the Docker Image locally

You will build the docker image locally to see if it works perfectly and then automate this process using GitHub Actions.

a. command to build the docker image

docker build . -t cpp_project:v0.0.1

b. command to run the docker image

docker run cpp_project:v0.0.1

Follow this Step to Deploy Docker Image to Azure Container Registry (ACR) and deploy the Docker Image to Azure App Service using GitHub Actions

Step 1: Authenticate with Azure using OIDC.

Using OIDC (OpenID Connect) allows secure, short-lived and GitHub managed identities to access Azure resources without storing secrets in GitHub. OIDC uses federated tokens, which mean that Azure issues a token only when it is needed and only to a trusted identify providers (GitHub or GitLab). By using OIDC, this reduces risk of credentials getting leaks since OIDC will generate short lived credentials (temporally).

a. Login to the azure account using the appropriate Azure subscription

# Login to your azure account and select the subscription
az login

b. Set the active subscription for your Azure CLI session

Replace <subscription_id> with your active subscription. The command below will set active subscription for your Azure CLI.

az account set --subscription <subscription_id>

c. Create a Federated Identity and Role Assignment.

Run this script below which will allow your GitHub Actions workflow to authenticate with Azure using GitHub OIDC (OpenID Connect), it will allow you to push the Docker Image to ACR (Azure Container Registry) and be able to deploy the Docker image to Azure App Service.

#!/bin/bash

set -e # Exit on any error

# Variables
ACR_NAME="cplusplus"
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
RESOURCE_GROUP="c-plus-plus-project"
AZURE_TENANT_ID=$(az account show --query tenantId -o tsv)
AZURE_AD_APP_NAME="github-c-plus-plus-oidc"
GITHUB_REPO="ExitoLab/docker_c_plus_plus_github_action_azure_app_service_example"
LOCATION="eastus"

echo "Starting Azure setup for GitHub OIDC..."
echo "Subscription: $SUBSCRIPTION_ID"
echo "Tenant: $AZURE_TENANT_ID"
echo "GitHub Repo: $GITHUB_REPO"

# Create Resource group if it does not exist
echo "Creating resource group..."
az group create --name $RESOURCE_GROUP --location $LOCATION

# Create ACR
echo "Creating Azure Container Registry..."
az acr create --name $ACR_NAME
--resource-group $RESOURCE_GROUP
--sku Basic
--location $LOCATION
--admin-enabled true

# Create app registration
echo "Creating Azure AD app registration..."
az ad app create --display-name "$AZURE_AD_APP_NAME"

# Get clientId
APP_ID=$(az ad app list --display-name "$AZURE_AD_APP_NAME" --query "[0].appId" -o tsv)

if [ -z "$APP_ID" ]; then
echo "Error: Failed to create or retrieve app registration"
exit 1
fi

echo "App ID: $APP_ID"

# Create service principal from the app ID (skip if already exists)
echo "Creating service principal..."
if ! az ad sp show --id $APP_ID &>/dev/null; then
az ad sp create --id $APP_ID
echo "Service principal created."
else
echo "Service principal already exists, skipping creation."
fi

# Create federated credentials for different scenarios
echo "Creating federated credentials..."

# Main branch
az ad app federated-credential create --id $APP_ID --parameters '{
"name": "github-oidc-main-container-app",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:'"$GITHUB_REPO"':ref:refs/heads/main",
"description": "OIDC GitHub Main Branch",
"audiences": ["api://AzureADTokenExchange"]
}'

# Pull requests
az ad app federated-credential create --id $APP_ID --parameters '{
"name": "github-oidc-pr-container-app",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:'"$GITHUB_REPO"':pull_request",
"description": "OIDC GitHub Pull Requests",
"audiences": ["api://AzureADTokenExchange"]
}'

# Get ACR resource ID
ACR_ID=$(az acr show --name $ACR_NAME --query id -o tsv)

# Assign ACR roles
echo "Assigning ACR permissions..."
az role assignment create --assignee $APP_ID
--role "AcrPush"
--scope $ACR_ID

az role assignment create --assignee $APP_ID
--role "AcrPull"
--scope $ACR_ID

# Assign Contributor role to allow creating App Service in GitHub Actions
echo "Assigning Contributor role on the Resource Group..."
az role assignment create
--assignee $APP_ID
--role "Contributor"
--scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP"

# Get ACR login server
ACR_LOGIN_SERVER=$(az acr show --name $ACR_NAME --query loginServer -o tsv)

echo ""
echo "=== Setup Complete! ==="
echo ""
echo "Add these secrets to your GitHub repository:"
echo "AZURE_CLIENT_ID: $APP_ID"
echo "AZURE_TENANT_ID: $AZURE_TENANT_ID"
echo "AZURE_SUBSCRIPTION_ID: $SUBSCRIPTION_ID"
echo "ACR_LOGIN_SERVER: $ACR_LOGIN_SERVER"
echo ""
echo "Your GitHub Actions workflow can now authenticate with Azure using OIDC!"
echo "ACR Name: $ACR_NAME"
echo "Resource Group: $RESOURCE_GROUP"

Step 2: Create GitHub Actions and use the OIDC

Once you run the script above it will generate the following

echo "AZURE_CLIENT_ID: $APP_ID"
echo "AZURE_TENANT_ID: $AZURE_TENANT_ID"
echo "AZURE_SUBSCRIPTION_ID: $SUBSCRIPTION_ID"

a. Create a secrets in GitHub similar with screenshot below

b. Below is a copy of the GitHub Action deploy workflow

The GitHub Actions pipeline fully automates the C++ project by performing the following:

✅ Build and test your C++ application. By running the test script
✅ Package the C++ project into a Docker container
✅ Push the Docker image to Azure Container Registry (ACR)
✅ Deploy that Docker image into Azure Web App for Containers
✅ All fully automated after every push or pull_request to main branch

There are two jobs in the GitHub Actions workflow. test and build jobs.

  • Test job build and test your C++ project before creating the Docker image
  • Build job build the docker image, push the docker image to ACR and deploy it to Azure Web App.
name: Build C++ Docker Image, Run Test, Push to ACR  and Deploy to Azure Web App

on:
push:
branches:
- main
paths:
- '**'
- '!README.md'
- '!docs/**'
pull_request:
branches:
- main
paths:
- '**'
- '!README.md'
- '!docs/**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y g++ libgtest-dev cmake libboost-system-dev libboost-date-time-dev

- name: Clone Crow library
run: git clone https://github.com/CrowCpp/Crow.git

- name: Clone Asio library
run: git clone https://github.com/chriskohlhoff/asio.git

- name: Create Build Directory
run: mkdir build

- name: Configure CMake
run: |
cd build
cmake -DCROW_INCLUDE_DIR=../Crow/include -DASIO_INCLUDE_DIR=../asio/asio/include ..

- name: Build
run: cd build && make

- name: Run tests
run: ./build/test_project

build:
runs-on: ubuntu-latest
needs: [test]
env:
ACR_NAME: cplusplus
IMAGE_NAME: cplusplus
RESOURCE_GROUP: c-plus-plus-project
LOCATION: eastus

permissions:
id-token: write
contents: read
pull-requests: write
repository-projects: write

steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 #v3.10.0

- name: Azure Login using OIDC
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef51 #v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Login to Azure Container Registry
run: az acr login --name ${{ env.ACR_NAME }}

- name: Extract metadata for Docker
id: meta
run: |
echo "date=$(date +'%Y%m%d-%H%M%S')" >> $GITHUB_OUTPUT
echo "tag=${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.sha }}" >> $GITHUB_OUTPUT

- name: Build C++ Docker image
run: |
docker build -t ${{ steps.meta.outputs.tag }} .

- name: Push image to ACR
run: docker push ${{ steps.meta.outputs.tag }}

- name: Show pushed image
run: |
echo "Image pushed:"
az acr repository show-tags --name ${{ env.ACR_NAME }} --repository ${{ env.IMAGE_NAME }} --output table


- name: Create App Service Plan (if it doesn't exist)
run: |
if ! az appservice plan show --name cplusplus-plan --resource-group ${{ env.RESOURCE_GROUP }} &> /dev/null; then
echo "Creating App Service Plan..."
az appservice plan create
--name cplusplus-plan
--resource-group ${{ env.RESOURCE_GROUP }}
--is-linux
--sku F1
--location eastus2
else
echo "App Service Plan already exists"
fi

- name: Generate Unique App Name
id: unique_name
run: |
UNIQUE_NAME="cplusplus-${{ github.run_number }}-$(echo ${{ github.sha }} | cut -c1-8)"
echo "webapp_name=$UNIQUE_NAME" >> $GITHUB_OUTPUT
echo "Generated unique app name: $UNIQUE_NAME"

- name: Create App Service with ACR image (if it doesn't exist)
run: |
WEBAPP_NAME="${{ steps.unique_name.outputs.webapp_name }}"

if ! az webapp show --resource-group ${{ env.RESOURCE_GROUP }} --name $WEBAPP_NAME &> /dev/null; then
echo "Creating App Service: $WEBAPP_NAME"

# Create Web App with container image
az webapp create
--resource-group ${{ env.RESOURCE_GROUP }}
--plan cplusplus-plan
--name $WEBAPP_NAME
--deployment-container-image-name ${{ steps.meta.outputs.tag }}

echo "Web App created successfully: $WEBAPP_NAME"

# Now configure the registry credentials
echo "Configuring ACR credentials..."

# Get ACR credentials
ACR_USERNAME=$(az acr credential show --name ${{ env.ACR_NAME }} --query username --output tsv)
ACR_PASSWORD=$(az acr credential show --name ${{ env.ACR_NAME }} --query passwords[0].value --output tsv)

# Configure container registry settings
az webapp config container set
--resource-group ${{ env.RESOURCE_GROUP }}
--name $WEBAPP_NAME
--docker-custom-image-name ${{ steps.meta.outputs.tag }}
--docker-registry-server-url https://${{ env.ACR_NAME }}.azurecr.io
--docker-registry-server-user $ACR_USERNAME
--docker-registry-server-password $ACR_PASSWORD

echo "ACR credentials configured"
echo "Waiting for App Service to be ready..."
sleep 10
else
echo "App Service already exists: $WEBAPP_NAME"
fi

# Store the webapp name for subsequent steps
echo "WEBAPP_NAME=$WEBAPP_NAME" >> $GITHUB_ENV

- name: Update App Service container image
run: |
WEBAPP_NAME="${{ env.WEBAPP_NAME }}"

# Ensure we have a webapp name
if [ -z "$WEBAPP_NAME" ]; then
echo "ERROR: WEBAPP_NAME is not set"
exit 1
fi

echo "Updating App Service: $WEBAPP_NAME with new container image..."
echo "New image: ${{ steps.meta.outputs.tag }}"

# Get ACR credentials
echo "Getting ACR credentials..."
ACR_USERNAME=$(az acr credential show --name ${{ env.ACR_NAME }} --query username --output tsv)
ACR_PASSWORD=$(az acr credential show --name ${{ env.ACR_NAME }} --query passwords[0].value --output tsv)

if [ -z "$ACR_USERNAME" ] || [ -z "$ACR_PASSWORD" ]; then
echo "ERROR: Failed to get ACR credentials"
exit 1
fi

# Update container settings with new image
echo "Updating container configuration..."
az webapp config container set
--resource-group ${{ env.RESOURCE_GROUP }}
--name "$WEBAPP_NAME"
--docker-custom-image-name "${{ steps.meta.outputs.tag }}"
--docker-registry-server-url "https://${{ env.ACR_NAME }}.azurecr.io"
--docker-registry-server-user "$ACR_USERNAME"
--docker-registry-server-password "$ACR_PASSWORD"

echo "Container image updated successfully to: ${{ steps.meta.outputs.tag }}"

- name: Restart App Service
run: |
echo "Restarting App Service to apply new container..."
az webapp restart --resource-group ${{ env.RESOURCE_GROUP }} --name ${{ env.WEBAPP_NAME }}

- name: Show deployment info
run: |
echo "Deployment completed!"
echo "App Service URL: https://${{ env.WEBAPP_NAME }}.azurewebsites.net"
az webapp show --resource-group ${{ env.RESOURCE_GROUP }} --name ${{ env.WEBAPP_NAME }} --query defaultHostName --output tsv

- name: Azure logout
run: az logout
if: always()

c. Access the C++ project from your browser

Once the GitHub Action has been successfully executed, it will generate a URL which will be used to access the application.

Below is a copy of the screen

Conclusion

This article walks you through setting up a C++ application, containerizing it with Docker, automating the CI/CD pipeline using GitHub Actions, finally pushing the container image to Azure Container Registry (ACR) and deploying to Azure App Service.

It explained the basic workflow for building and deploying a C++ application and deploying it to Azure App service.

You also learned how to securely run workflows on GitHub Actions using OIDC (OpenID Connect) to authenticate with your Azure account.

This article explains how to deploy a C++ Docker container to Azure Container Instance https://igeadetokunbo.medium.com/building-a-todo-list-restful-api-in-c-with-docker-github-actions-azure-acr-a-beginners-guide-05e996435782

Difference between Azure App Service and Azure Container Instances, Azure App Service is a fully managed Platform as a Service (PaaS), for hosting web apps, API and backend while Azure Container Instances is a Serverless container as a Service, running containers directly without managing infrastructure.

Check out the completed code on GitHub

Original post (opens in new tab)

Rate

You rated this post out of 5. Change rating

Share

Share

Rate

You rated this post out of 5. Change rating