Create an Azure Pipelines to deploy Docker image for Azure App Service
Kevin Le
Posted on August 6, 2021
This article details how to containerize an app, create an App Service in Azure and configure it to pull the Docker image from the Azure Container Registry (ACR). How the Docker image gets deployed from our code to ACR is a result of us creating a deployment pipeline using Azure Pipelines.
The benefit is instead of manually pushing the locally-built Docker image to ACR after some code changes, we now have an automated Continuous Integration/Continuous Deployment (CI/CD) process.
The workflow can be described as follow: We write the code on our Dev computer, build and run the Docker image locally. Once we do a git
push of the code to a git
repo, Azure Pipelines performs the build on some agent machine and deploys the Docker image to ACR. Since the App Service has also already been configured for Continuous Deployment, it will pull the image from the ACR automatically. Essentially we have a pipeline from code to repo to Docker image to ACR to App Service.
1) We start with writing an app. What app to write is not the focal point of this article. But to have something to start with, we will create an out-of-the-box NextJS app using npx create-next-app
. We could have done with an ASP.Net core app and everything will be just the same. So let's start:
npx create-next-app mysample
cd mysample
npm run build
2) We will create a Docker image for this and run in a local Docker container. To do so, we will create a Dockerfile
and a docker-compose.yml
file.
Here's the Dockerfile
FROM node:14 as BUILD_IMAGE
WORKDIR /var/www/app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN yarn build
# remove dev dependencies
RUN npm prune --production
FROM node:14
WORKDIR /var/www/app
COPY --from=BUILD_IMAGE /var/www/app/package.json ./package.json
COPY --from=BUILD_IMAGE /var/www/app/node_modules ./node_modules
COPY --from=BUILD_IMAGE /var/www/app/.next ./.next
COPY --from=BUILD_IMAGE /var/www/app/public ./public
EXPOSE 3000
CMD ["yarn", "start"]
And docker-compose.yml
file
version: '3.4'
services:
app:
build:
context: .
dockerfile: ./Dockerfile
ports:
- 80:3000
env_file: .env
command:
sh -c 'yarn start'
3) Now we can run the Docker locally with the command
up --build
It should work if you point the browser to http://localhost
(that's port 80 not 3000)
4) All the next steps that we're about to do can be done with the UI in Azure Portal, but we will use Azure CLI as much as possible. Also, I will name my Resource Group
and App Service Plan
as MyResourceGroup
and MyLinuxPlan
respectively. If you already have a Resource Group and App Service plan, you can continue using them. Or you can follow next step and substitute with names of your own choosing.
Let's create a Resource Group called MyResourceGroup
az group create -l westus -n MyResourceGroup
Now we create a Linux App Service Plan called MyLinuxPlan
with 2 workers
az appservice plan create -g MyResourceGroup -n MyLinuxPlan \
--is-linux --number-of-workers 2 --sku S1
To create a Resource Group, refer to this link.
To create an App Service Plan refer to this link
5) Now we create an Azure Container Registry called mysampleacr
az acr create --name mysampleacr --resource-group \
MyResourceGroup --sku Basic --admin-enabled true
At this point, we now have an ACR but no image has been pushed to it and no App Service has been created. This is where my approach starts to differ from that in Azure documentation.
This is OK because we don't want to push the Docker image to ACR manually. Instead we will set up the Azure Pipeline now.
6) Let's go to https://dev.azure.com, navigate to the Pipeline
page and create a new Pipeline as shown in the next picture:
7) Then specify Where is you code?
when prompted.
8) At the page Configure your pipeline
, scroll down and click on "Show more". Then select Docker Build and push an image to Azure Container Registry
9) Select your Subscription and Continue
. You might be prompted to login again.
10) Now select your Container Registry. If you follow this example, earlier we name it mysampleacr
. For image name, let's name it mysampleimg
. Then click on Validate and configure
11) We should get a generated azure-pipelines.yml
file with contents similar to the one below
# Docker
# Build and push an image to Azure Container Registry
# https://docs.microsoft.com/azure/devops/pipelines/languages/docker
trigger:
- main
resources:
- repo: self
variables:
# Container registry service connection established during pipeline creation
dockerRegistryServiceConnection: 'b0...a4'
imageRepository: 'mysampleimg'
containerRegistry: 'mysampleacr.azurecr.io'
dockerfilePath: '**/Dockerfile'
tag: '$(Build.BuildId)'
# Agent VM image name
vmImageName: 'ubuntu-latest'
stages:
- stage: Build
displayName: Build and push stage
jobs:
- job: Build
displayName: Build
pool:
vmImage: $(vmImageName)
steps:
- task: Docker@2
displayName: Build and push an image to container registry
inputs:
command: buildAndPush
repository: $(imageRepository)
dockerfile: $(dockerfilePath)
containerRegistry: $(dockerRegistryServiceConnection)
tags: |
$(tag)
We need to make a couple of changes and then we can Save and run
.
In the above file, look for the line tag: '$(Build.BuildId)'
and add a line below it. It will now show
tag: '$(Build.BuildId)'
latestTag: 'latest'
In the last 2 lines of the file, change from
tags: |
$(tag)
to
tags: |
$(latestTag)
The resulting/modified azure-pipelines.yml
file should now look something like
# Docker
# Build and push an image to Azure Container Registry
# https://docs.microsoft.com/azure/devops/pipelines/languages/docker
trigger:
- main
resources:
- repo: self
variables:
# Container registry service connection established during pipeline creation
dockerRegistryServiceConnection: 'b0...a4'
imageRepository: 'mysampleimg'
containerRegistry: 'mysampleacr.azurecr.io'
dockerfilePath: '**/Dockerfile'
tag: '$(Build.BuildId)'
latestTag: 'latest'
# Agent VM image name
vmImageName: 'ubuntu-latest'
stages:
- stage: Build
displayName: Build and push stage
jobs:
- job: Build
displayName: Build
pool:
vmImage: $(vmImageName)
steps:
- task: Docker@2
displayName: Build and push an image to container registry
inputs:
command: buildAndPush
repository: $(imageRepository)
dockerfile: $(dockerfilePath)
containerRegistry: $(dockerRegistryServiceConnection)
tags: |
$(latestTag)
Now we can Save and run
12) When the run finishes, we can verify there is an image in the ACR by running the following commands (one by one)
az acr repository list -n mysampleacr
az acr repository show -n mysampleacr \
--repository mysampleimg
az acr repository show -n mysampleacr \
--repository mysampleimg:latest
For more help on az acr repository
commands, please refer to https://docs.microsoft.com/en-us/cli/azure/acr/repository?view=azure-cli-latest#az_acr_repository_list
13) Now we can create an App Service and tell it to pull from the latest image mysampleimg:latest
from the ACR mysampleacr
.
az webapp create --resource-group MyResourceGroup --plan MyLinuxPlan --name mysample --deployment-container-image-name mysampleacr.azurecr.io/mysampleimg:latest
14) To ensure when a new image is pushed to the ACR, the App Service will automatically pull the latest image, go to Azure Portal, navigate to the App Service blade and make sure Continuous Deployment
is on.
References (A little difficult to read for me, but that's just my opinion):
https://docs.microsoft.com/en-us/azure/app-service/quickstart-nodejs?pivots=platform-linux
https://docs.microsoft.com/en-us/azure/architecture/example-scenario/apps/devops-dotnet-webapp
Posted on August 6, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.