Simple CI/CD for Azure Web App with Docker-compose and GitHub Actions ☁️🐋

Dömötör Lugosi
7 min readMay 21, 2024

--

In this article, we will explore how to set up a Continuous Integration and Continuous Deployment (CI/CD) pipeline using GitHub Actions to build and deploy a Dockerized application to an Azure Web App. The pipeline will handle both front-end and back-end builds, Docker image creation, and deployment to Azure. Let’s dive into the details.

Prerequisites

Before getting started, ensure you have the following:

  • An Azure account
  • An Azure Container Registry (ACR)
  • An Azure Web App for Containers
  • A GitHub repository with your application code
  • GitHub Secrets configured with your Azure credentials

Folder Structure

Your project should have the following structure:

├── .github
│ └── workflows
│ └── azure-cicd-dev.yml

├── front
│ ├── Dockerfile
│ └── # other front-end related files

├── back
│ ├── Dockerfile
│ └── # other back-end related files

├── docker-compose-azure-dev.yml
└── # other project files
  • The front directory contains the Dockerfile and other files related to the front-end application.
  • The back directory contains the Dockerfile and other files related to the back-end application.
  • The docker-compose-azure-dev.yml file is at the root of the project and defines the services and their configurations for the Docker Compose setup.

Front-End Dockerfile (front/Dockerfile)

# Use the official node image as the base image
FROM node:14-alpine

# Set the working directory
WORKDIR /app

# Copy package.json and package-lock.json
COPY package*.json ./

# Install dependencies
RUN npm ci

# Copy the rest of the application code
COPY . .

# Build the application
RUN npm run build

# Expose the port the app runs on
EXPOSE 3000

# Command to run the app
CMD ["npm", "start"]

Back-End Dockerfile (back/Dockerfile)

# Use the official node image as the base image
FROM node:14-alpine

# Set the working directory
WORKDIR /app

# Copy package.json and package-lock.json
COPY package*.json ./

# Install dependencies
RUN npm ci

# Copy the rest of the application code
COPY . .

# Expose the port the app runs on
EXPOSE 8080

# Command to run the app
CMD ["npm", "start"]

Docker Compose File (docker-compose-azure-dev.yml)

version: '3.8'

services:
frontend:
image: myacr.azurecr.io/frontend:latest
ports:
- "80:3000"
environment:
- NODE_ENV=dev

backend:
image: myacr.azurecr.io/backend:latest
ports:
- "8080:8080"
environment:
- NODE_ENV=dev

Setting Up the GitHub Actions Workflow

We’ll create a GitHub Actions workflow file named azure-cicd-dev.yml in the .github/workflows directory of your repository. This workflow will have two main jobs: npm-build and docker-build-and-deploy.

Workflow Dispatch Inputs

The workflow can be manually triggered with the following inputs:

  • run_npm_builds: A boolean to decide whether to run npm builds.
  • additional_string_for_docker_tag: An optional string to append to the Docker tag.
name: DEV compose build and deploy to Azure Web App

on:
workflow_dispatch:
inputs:
run_npm_builds:
description: 'Run npm builds'
required: true
type: choice
default: 'false'
options:
- 'true'
- 'false'
additional_string_for_docker_tag:
description: 'Additional string for Docker tag'
required: false
type: string

Job 1: NPM Build

The npm-build job runs only if run_npm_builds is set to true. It checks out the code and builds both the front-end and back-end using npm.

jobs:
npm-build:
runs-on: ubuntu-latest
if: ${{ github.event.inputs.run_npm_builds == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Build frontend
run: |
cd front
npm ci
npm run build

- name: Build backend
run: |
cd back
npm ci
npm run build

Job 2: Docker Build and Deploy

The docker-build-and-deploy job depends on the npm-build job and runs after it, or immediately if npm-build is not executed. This job involves several steps:

  1. Check out the code.
  2. Login to Azure CLI.
  3. Set environment variables for Docker tags and the agent’s IP address.
  4. Whitelist the agent IP in the Azure Container Registry (ACR).
  5. Build and push Docker images for both the front-end and back-end.
  6. Update the Docker Compose file with the new image tags.
  7. Deploy the Docker Compose file to Azure Web App.
  8. Commit and push changes to the repository.
  9. Logout from Azure CLI.
  docker-build-and-deploy:
runs-on: ubuntu-latest
needs: npm-build
if: ${{ github.event.inputs.run_npm_builds == 'false' || success() }}
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
ref: ${{ github.ref }}

- name: AZ CLI login
uses: azure/login@v1
with:
creds: '{"clientId":"${{ secrets.CLIENT_ID }}","clientSecret":"${{ secrets.CLIENT_SECRET }}","subscriptionId":"${{ secrets.SUBSCRIPTION_ID }}","tenantId":"${{ secrets.TENANT_ID }}"}'

- name: Set variables
id: vars
run: |
echo "::set-output name=docker_tag::$(git rev-parse --short HEAD)-$(TZ='Europe/Paris' date +%Y-%m-%d-%H-%M)"
echo "::set-output name=agent_ip::$(dig +short myip.opendns.com @resolver1.opendns.com)"

- name: Whitelist agent ip
run: |
if [ -z "$(az acr network-rule list --name ${{ vars.ACR_REPO_SHORT }} --resource-group ${{ vars.ACR_REPO_RG }} | grep ${{ steps.vars.outputs.agent_ip }} )"]
then
echo "Adding agent IP ${{ steps.vars.outputs.agent_ip }} to Azure Container Registry firewall whitelist"
az acr network-rule add --name ${{ vars.ACR_REPO_SHORT }} --resource-group ${{ vars.ACR_REPO_RG }} --ip-address ${{ steps.vars.outputs.agent_ip }}
else
echo "Agent is already whitelisted; skipping."
fi

- uses: azure/docker-login@v1
with:
login-server: ${{ vars.ACR_REPO }}
username: ${{ secrets.DOCKERIO_USERNAME }}
password: ${{ secrets.DOCKERIO_PASSWORD }}
- run: |
docker build -t ${{ vars.ACR_REPO }}/frontend:${{ steps.vars.outputs.docker_tag }}${{ github.event.inputs.additional_string_for_docker_tag }} -f front/Dockerfile .
docker push ${{ vars.ACR_REPO }}/frontend:${{ steps.vars.outputs.docker_tag }}${{ github.event.inputs.additional_string_for_docker_tag }}

docker build -t ${{ vars.ACR_REPO }}/backend:${{ steps.vars.outputs.docker_tag }}${{ github.event.inputs.additional_string_for_docker_tag }} -f back/Dockerfile .
docker push ${{ vars.ACR_REPO }}/backend:${{ steps.vars.outputs.docker_tag }}${{ github.event.inputs.additional_string_for_docker_tag }}

- name: Remove whitelisted agent ip
if: always()
run: |
echo "Removing agent IP ${{ steps.vars.outputs.agent_ip }} from Azure Container Registry firewall whitelist"
az acr network-rule remove --name ${{ vars.ACR_REPO_SHORT }} --resource-group ${{ vars.ACR_REPO_RG }} --ip-address ${{ steps.vars.outputs.agent_ip }} --only-show-errors --output none

- name: Update the Dockercompose file
env:
FRONT_NEW_VERSION: '${{ vars.ACR_REPO }}\/frontend:${{ steps.vars.outputs.docker_tag }}${{ github.event.inputs.additional_string_for_docker_tag }}'
BACK_NEW_VERSION: '${{ vars.ACR_REPO }}\/backend:${{ steps.vars.outputs.docker_tag }}${{ github.event.inputs.additional_string_for_docker_tag }}'
run: |
sed -i 's/^ *image: .*frontend:.*$/ image: '$FRONT_NEW_VERSION'/' docker-compose-azure-dev.yml
sed -i 's/^ *image: .*backend:.*$/ image: '$BACK_NEW_VERSION'/' docker-compose-azure-dev.yml


- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v2
with:
app-name: ${{ vars.DEV_APP_NAME }}
configuration-file: 'docker-compose-azure-dev.yml'

- name: Commit files
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git commit -m "Bump versions in docker-compose-azure" docker-compose-azure-dev.yml

- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.PAT }}

- name: AZ CLI logout
if: always()
run: |
az logout

Full Code

Here is the full code for the GitHub Actions workflow file azure-cicd-dev.yml:

name: DEV compose build and deploy to Azure Web App

on:
workflow_dispatch:
inputs:
run_npm_builds:
description: 'Run npm builds'
required: true
type: choice
default: 'fasle'
options:
- 'true'
- 'false'
additional_string_for_docker_tag:
description: 'Additional string for Docker tag'
required: false
type: string

jobs:
npm-build:
runs-on: ubuntu-latest
if: ${{ github.event.inputs.run_npm_builds == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Build frontend
run: |
cd front
npm ci
npm run build

- name: Build backend
run: |
cd back
npm ci
npm run build

docker-build-and-deploy:
runs-on: ubuntu-latest
needs: npm-build
if: ${{ github.event.inputs.run_npm_builds == 'false' || success() }}
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
ref: ${{ github.ref }}

- name: AZ CLI login
uses: azure/login@v1
with:
creds: '{"clientId":"${{ secrets.CLIENT_ID }}","clientSecret":"${{ secrets.CLIENT_SECRET }}","subscriptionId":"${{ secrets.SUBSCRIPTION_ID }}","tenantId":"${{ secrets.TENANT_ID }}"}'

- name: Set variables
id: vars
run: |
echo "::set-output name=docker_tag::$(git rev-parse --short HEAD)-$(TZ='Europe/Paris' date +%Y-%m-%d-%H-%M)"
echo "::set-output name=agent_ip::$(dig +short myip.opendns.com @resolver1.opendns.com)"

- name: Whitelist agent ip
run: |
if [ -z "$(az acr network-rule list --name ${{ vars.ACR_REPO_SHORT }} --resource-group ${{ vars.ACR_REPO_RG }} | grep ${{ steps.vars.outputs.agent_ip }} )"]
then
echo "Adding agent IP ${{ steps.vars.outputs.agent_ip }} to Azure Container Registry firewall whitelist"
az acr network-rule add --name ${{ vars.ACR_REPO_SHORT }} --resource-group ${{ vars.ACR_REPO_RG }} --ip-address ${{ steps.vars.outputs.agent_ip }}
else
echo "Agent is already whitelisted; skipping."
fi

- uses: azure/docker-login@v1
with:
login-server: ${{ vars.ACR_REPO }}
username: ${{ secrets.DOCKERIO_USERNAME }}
password: ${{ secrets.DOCKERIO_PASSWORD }}
- run: |
docker build -t ${{ vars.ACR_REPO }}/frontend:${{ steps.vars.outputs.docker_tag }}${{ github.event.inputs.additional_string_for_docker_tag }} -f front/Dockerfile .
docker push ${{ vars.ACR_REPO }}/frontend:${{ steps.vars.outputs.docker_tag }}${{ github.event.inputs.additional_string_for_docker_tag }}

docker build -t ${{ vars.ACR_REPO }}/backend:${{ steps.vars.outputs.docker_tag }}${{ github.event.inputs.additional_string_for_docker_tag }} -f back/Dockerfile .
docker push ${{ vars.ACR_REPO }}/backend:${{ steps.vars.outputs.docker_tag }}${{ github.event.inputs.additional_string_for_docker_tag }}

- name: Remove whitelisted agent ip
if: always()
run: |
echo "Removing agent IP ${{ steps.vars.outputs.agent_ip }} from Azure Container Registry firewall whitelist"
az acr network-rule remove --name ${{ vars.ACR_REPO_SHORT }} --resource-group ${{ vars.ACR_REPO_RG }} --ip-address ${{ steps.vars.outputs.agent_ip }} --only-show-errors --output none

- name: Update the Dockercompose file
env:
FRONT_NEW_VERSION: '${{ vars.ACR_REPO }}\/frontend:${{ steps.vars.outputs.docker_tag }}${{ inputs.additional_string_for_docker_tag }}'
BACK_NEW_VERSION: '${{ vars.ACR_REPO }}\/backend:${{ steps.vars.outputs.docker_tag }}${{ inputs.additional_string_for_docker_tag }}'
run: |
sed -i 's/^ *image: .*frontend:.*$/ image: '$FRONT_NEW_VERSION'/' docker-compose-azure-dev.yml
sed -i 's/^ *image: .*backend:.*$/ image: '$BACK_NEW_VERSION'/' docker-compose-azure-dev.yml


- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v2
with:
app-name: ${{ vars.DEV_APP_NAME }}
configuration-file: 'docker-compose-azure-dev.yml'

- name: Commit files
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git commit -m "Bump versions in docker-compose-azure" docker-compose-azure-dev.yml

- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.PAT }}

- name: AZ CLI logout
if: always()
run: |
az logout

Summary

This GitHub Actions workflow enables a seamless CI/CD process for deploying a Dockerized application to an Azure Web App. By setting up the necessary jobs and steps, you can automate the build, push, and deployment processes, ensuring that your application is always up-to-date and running smoothly in the cloud.

Feel free to customize this workflow further to fit your specific needs, and happy deploying! 🚢

🙌🏻 Thank you for reading! Your time and attention are greatly appreciated.

🚀 I’m grateful you stayed with me until the end. If you have questions or feedback about this blog, I’d love to hear from you!

💼 Connect with me on LinkedIn: Dömötör Lugosi

See you in the next post!

--

--