
CI/CD for Azure Web App
Summary
Astro website with CI/CD integration using services like Azure Devops, ACR & Web apps
Commits trigger a pipeline running through a self hosted agent pool to build the docker container, the container is then uploaded to an ACR and subsequently the web app to update it with the latest content
Detailed Steps
Web App -
Create an app service plan, I’m using the B1 plan as the container is lightweight and not much traffic is expected
With the B1 plan usage sits around 70% memory & 20% CPU usage
Create a web app pointed to the newly created app service plan, after deployment you can add a custom domain if desired
Custom Domain setup -
I opted to point my root https://andriusdalgeda.uk domain to this webapp
This involved adding a A & TXT record to verify my domain as well as binding a newly created certificate to the domain to allow HTTPS traffic backed by the certificate
Storage account & CDN setup -
I also created a storage account to store all the images on the site - the backend storage account holds blobs with all the images on the site and an Azure CDN sits in front providing the content The CDN is used as storage accounts on their own don’t support HTTPS traffic to a custom domain as there is no option to add a certificate, instead a CDN is used with a custom domain and an Azure managed certificate
The custom domain for the images is https://images.andriusdalgeda.uk
The CDN settings enable caching to speed up content delivery, geo-filtering can also be used to block traffic from certain locations
Azure Devops Setup -
Create a new project and also init a repo or upload existing code
Check that either self hosted agent pools or parallel jobs are configured and available
Setup pipeline using a YAML file, dockerfile and nginx.conf file
YAML
# Docker
trigger:
- master
resources:
- repo: self
variables:
- group: staticLib
stages:
- stage: Build
displayName: Build and push stage
jobs:
- job: Build
displayName: Build
pool:
name: desktop
steps:
- task: Docker@2
displayName: Build and push an image to container registry
inputs:
command: buildAndPush
repository: $(imageRepository)
dockerfile: $(dockerfilePath)
containerRegistry: $(dockerRegistryServiceConnection)
tags: |
$(tag)
- task: AzureWebAppContainer@1
displayName: 'Azure Web App on Container Deploy'
inputs:
azureSubscription: $(azureSubscription)
appName: $(appName)
containers: $(containerRegistry)/$(imageRepository):$(tag)
dockerfile
# Dockerfile (for static site)
# ---- Build Stage ----
# Use an official Node.js image as the base for building
# 'alpine' versions are smaller
FROM node:22-alpine AS builder
# Set the working directory inside the container
WORKDIR /app
# Copy package.json and lock file first to leverage Docker cache
COPY package*.json ./
# Install project dependencies
RUN npm install
# Copy the rest of the application code (respects .dockerignore)
COPY . .
# Build the Astro site for production
RUN npm run build
# The static files are now in /app/dist
# ---- Runtime Stage ----
# Use an official Nginx image as the base for serving
FROM nginx:1.27-alpine AS runtime
# Set the working directory for Nginx files
WORKDIR /usr/share/nginx/html
# Remove default Nginx welcome page
RUN rm -rf ./*
# Copy the built static files from the 'builder' stage
COPY --from=builder /app/dist .
# Copy our custom Nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port 80 (standard HTTP port)
EXPOSE 80
# Command to run Nginx in the foreground when the container starts
CMD ["nginx", "-g", "daemon off;"]
nginx.conf
# /nginx.conf
server {
listen 80;
server_name _; # Accepts any hostname
root /usr/share/nginx/html; # Root directory for static files
# Standard index file
index index.html;
# Error page for SPA routing (optional, but often useful)
# If a file is not found, try serving index.html instead
error_page 404 /index.html;
# Enable Gzip compression for text-based files
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml;
location / {
# Try serving the requested file, then directory, then fallback to index.html
try_files $uri $uri/ /index.html;
# Set long cache expiration for static assets (e.g., 1 year)
location ~* \.(?:css|js|svg|gif|png|jpg|jpeg|webp|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public";
access_log off; # Optional: Disable logging for static assets
}
}
# Deny access to hidden files (like .htaccess)
location ~ /\. {
deny all;
}
}
Ensure that the pipeline is set to use an online self hosted agent pool or parallel jobs (if applicable)
In this example I use a staticLib variable group to hold all of the vars, this is to streamline any future edits of variables as they would only need to be changed in a singular location
Any secrets/sensitive data should also be stored in a var group instead of being defined in the .yaml itself
Commit or run the pipeline manually, expected behavior is:
- Docker container is built and uploaded to the specified ACR
- Web app is then updated from the new image in the ACR
- Web app restarts and presents the new content to users