Azure, Azure, and more Azure - GitHub Actions CI/CD | Deploy containers to Azure Web App

I’ve been using this setup for my own blog, and I wanted to document the process of deploying a containerized application to Azure Web App using GitHub Actions. In a previous post, I covered how to use Azure Container Registry Tasks to trigger builds automatically when code changes. This time, I want to focus on the GitHub Actions approach, which gives you more control over the build and deployment pipeline directly from your repository.

Self-study links

.

Notes:

GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform that allows you to automate your build, test, and deployment pipeline. You can create workflows that build and test every pull request to your repository, or deploy merged pull requests to production.

Workflow

The idea is to create a CI/CD pipeline that builds a Docker image from our application, pushes it to Azure Container Registry (ACR), and deploys it to an Azure Web App. Every time we push code to the main branch, the pipeline will run automatically.

For our example, I’m going to use a React application scaffolded with Vite. But this approach works with any application you can containerize — a .NET API, a Node.js server, a static site generator like Hexo, etc.

Prerequisites

Create the app

If you already have an application, skip this step. Otherwise, let’s scaffold a new React app using Vite.

NOTE: create-react-app is no longer maintained. Vite is the recommended alternative for new React projects.

1
2
3
4
npm create vite@latest my-azure-app -- --template react
cd my-azure-app
npm install
npm run dev

Dockerize the app

Create a Dockerfile in the root of your project. We use a multi-stage build to keep the final image small — the first stage builds the application, and the second stage serves it with Nginx. This way, the final image only contains the static files and the web server, not the Node.js toolchain.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Stage 1: Build the app
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Serve with Nginx
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

If you are using React Router or any client-side routing, you need to configure Nginx to redirect all requests to index.html. Create a nginx.conf file in the root of your project:

1
2
3
4
5
6
7
8
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}

You can test the Docker image locally before deploying:

1
2
docker build -t my-azure-app .
docker run -p 8080:80 my-azure-app

Then go to http://localhost:8080 and make sure everything works.

Set up Azure resources

First, log in to Azure and create the resources we need. I’m going to use real names so you can see the full picture — just replace them with yours.

1
az login

Create a Resource Group:

1
az group create --name github-actions-rg --location eastus

Create an Azure Container Registry. NOTE: The name must be alphanumeric and between 5 and 50 characters. Also, enable admin access so GitHub Actions can authenticate with username/password.

1
2
3
4
5
az acr create \
--resource-group github-actions-rg \
--name myappacrregistry \
--sku Basic \
--admin-enabled true

Create an App Service Plan. This defines the compute resources for your Web App. You can use the F1 (Free) tier for testing.

1
2
3
4
5
az appservice plan create \
--name my-app-plan \
--resource-group github-actions-rg \
--is-linux \
--sku F1

Create the Azure Web App and link it to the ACR image:

1
2
3
4
5
az webapp create \
--name my-app-webapp \
--resource-group github-actions-rg \
--plan my-app-plan \
--deployment-container-image-name myappacrregistry.azurecr.io/my-azure-app:latest

Now, configure the Web App to pull images from your ACR. Get the ACR credentials first:

1
az acr credential show --name myappacrregistry

Then set the container configuration:

1
2
3
4
5
6
az webapp config container set \
--name my-app-webapp \
--resource-group github-actions-rg \
--registry-server-url https://myappacrregistry.azurecr.io \
--registry-server-user myappacrregistry \
--registry-server-password <password-from-previous-command>

Configure GitHub Secrets

Go to your GitHub repository > Settings > Secrets and variables > Actions and add the following secrets:

  1. ACR_USERNAME: The ACR admin username (from az acr credential show --name myappacrregistry)
  2. ACR_PASSWORD: The ACR admin password (same command)
  3. AZURE_WEBAPP_PUBLISH_PROFILE: Go to the Azure Portal > your Web App > Overview > click Download publish profile and paste the entire XML content as the secret value

NOTE: For production environments, Azure recommends using OpenID Connect (OIDC) with federated credentials instead of passwords. This approach is more secure because it doesn’t require storing long-lived credentials. You can check the official guide if you want to set it up.

Create the GitHub Actions workflow

Create the file .github/workflows/deploy.yml in your repository:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
name: Build and deploy container to Azure Web App

on:
push:
branches:
- main
workflow_dispatch:

env:
ACR_REGISTRY: myappacrregistry.azurecr.io
IMAGE_NAME: my-azure-app
APP_NAME: my-app-webapp

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v7

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to ACR
uses: docker/login-action@v4
with:
registry: ${{ env.ACR_REGISTRY }}
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }}

- name: Build and push image
uses: docker/build-push-action@v7
with:
context: .
push: true
tags: |
${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:latest

deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: production
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}

steps:
- name: Deploy to Azure Web App
id: deploy-to-webapp
uses: azure/webapps-deploy@v3
with:
app-name: ${{ env.APP_NAME }}
slot-name: production
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
images: ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

A few things to notice about this workflow:

  • We use Docker Buildx for improved build performance and caching capabilities
  • We push two tags: the commit SHA for traceability and latest for the Web App to pick up the most recent image
  • The build and deploy are separate jobs. This way, if the build fails, we don’t attempt deployment. The needs: build ensures the deploy job waits for the build to complete
  • The workflow_dispatch trigger allows you to run the pipeline manually from the GitHub Actions tab
  • We use a publish profile for deployment authentication. This is simpler than configuring a service principal and works well for single-app deployments

Push and verify

Commit your changes and push to the main branch:

1
2
3
git add .
git commit -m "Add CI/CD pipeline with GitHub Actions"
git push origin main

Go to the Actions tab in your GitHub repository and you should see the workflow running. If both jobs turn green, your app is deployed.

You can find your Web App URL in the Azure Portal under your Web App > Overview. It should be something like https://my-app-webapp.azurewebsites.net.

NOTE: The first deployment might take a few minutes because Azure needs to pull the image and start the container. Subsequent deployments will be faster.

Issues I had

If you run into problems, here are some issues I encountered:

  • “Admin user is disabled” error when pushing to ACR: Make sure admin access is enabled. Run az acr update -n myappacrregistry --admin-enabled true to enable it. You can also check it in the Azure Portal under your ACR > Access keys.

  • “Application not registered with AAD” when deploying: Check that your publish profile is correct and hasn’t expired. Download a fresh one from the Azure Portal. If you’re using a service principal instead, make sure it has the Contributor role on the Web App and AcrPush on the ACR.

  • Container fails to start on Azure Web App: Check the container logs in the Azure Portal under your Web App > Deployment Center > Logs. Common issues include wrong port configuration (Azure expects port 80 or 8080 by default) and missing environment variables.

  • 404 errors with React Router: Make sure your nginx.conf file has the try_files $uri $uri/ /index.html; directive. Without it, Nginx won’t redirect deep links to your React application.

  • Image not found during deploy: This usually happens when the image tag in the deploy step doesn’t match what was pushed in the build step. Make sure both steps use the same ${{ github.sha }} tag.

ACR Tasks vs GitHub Actions

In my previous post about containers, I covered how to use Azure Container Registry Tasks to trigger builds and deployments automatically. Both approaches are valid, but here’s my take:

  • ACR Tasks is great when you want Azure to handle everything — the build happens inside Azure, so you don’t need a GitHub Actions runner. It’s simpler if your pipeline is just “build and deploy”. But I had some issues getting it to work with private repositories and custom Dockerfile paths.

  • GitHub Actions gives you more flexibility. You can add linting, testing, notifications, and any other step you need. You also get better visibility into what’s happening through the GitHub UI. For most projects, this is what I would recommend.

Useful Links


Thanks for reading. I hope you find it useful.

❤️