Deploying Next JS Web App using Docker and Github Actions in Azure

Natalie P
coding spaghetti
Published in
7 min readNov 21, 2023

We will create a web app, enable access to your repo, and create a github actions workflow. Later we will add code to dockerize your code base and have it deploy to the web app.

This took me hours to figure you since there were so many azure setup options. Even though you may have added docker login credentials to your github workflow yaml, it doesn’t mean your web app has access to the repo. Therefore configurations needed to be set to get everything working. I’m showing you a flow that makes as much of this setup automatic. There are other ways of doing this and/or you can create a web app via command line. This is just one way.

  1. First make sure yarn build & yarn start works locally. This will save you from debugging deployment if this is the reason your build is failing.
  2. Create a web app

2. Set a resource group. Give your app a name & select Docker Container as the Publish option. Select the region that closest/cheapest to your users. Press review + create button.

3. In the Docker Tab select your image source. In my case I had the docker image of my repo pushed to Azure. Therefore I only needed to select where the location of the image.

The advantage of already having an image pushed to your Azure container registry, is that it will preset the configurations to access docker for you. So you won’t need to manually put variables: DOCKER_REGISTRY_SERVER_URL , DOCKER_REGISTRY_SERVER_PASSWORD , DOCKER_REGISTRY_SERVER_USERNAME .

It doesn’t matter which image you select, as long as it’s an image from the registry you want access to.

If you don’t have an image yet, then you can skip this for now.

4. I enabled logging and then went to review + create tab and create the web app.

5. Once created goto the configuration tab on the left menu. Choose + New application setting.

Azure Web App Configuration View

If you had selected a docker image then the docker values would have been filled DOCKER_REGISTRY_SERVER_URL , DOCKER_REGISTRY_SERVER_PASSWORD , DOCKER_REGISTRY_SERVER_USERNAME.

Add the docker values if you didn’t select a repo above. This enables

You should also see WEBSITES_ENABLE_APP_SERVICE_STORAGE as false. If you don’t add it.

Also add WEBSITES_CONTAINER_START_TIME_LIMIT as 1800. Since it takes time to build the next js app, you will get a timeout error if you don’t do this.

6. Next go to the Deployment Center tab and select Github Actions

Grant access to your github repository by logging in with an owner account if your repo belongs to an organization.

Make sure workflow option, ‘add workflow’ is choosen. This will push a github workflow yml file that will run on the predefined branch and repo. What’s nice is that is will auto link the workflow with access credentials to the webapp.

7. Now goto your github repo & pull the to get the recent commit generated by the above.
Under .github/workflows/{branch}_{app_name}.yml you should see something similar to the this:

# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
# More GitHub Actions for Azure: https://github.com/Azure/actions

name: Build and deploy container app to Azure Web App - facets-feedback-docker

on:
push:
branches:
- main
workflow_dispatch:

jobs:
build:
runs-on: 'ubuntu-latest'

steps:
- uses: actions/checkout@v2

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

- name: Log in to registry
uses: docker/login-action@v2
with:
registry: https://scoopdevcontainerregistry.azurecr.io/
username: ${{ secrets.AzureAppService_ContainerUsername_7bb27a47a29b4c55a0946cf147f2b74a }}
password: ${{ secrets.AzureAppService_ContainerPassword_487e2a7c48c1428d9aee7922945def04 }}

- name: Build and push container image to registry
uses: docker/build-push-action@v3
with:
push: true
tags: scoopdevcontainerregistry.azurecr.io/${{ secrets.AzureAppService_ContainerUsername_7bb27a47a29b4c55a0946cf147f2b74a }}/feedback:${{ github.sha }}
file: ./Dockerfile

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@v2
with:
app-name: 'facets-feedback-docker'
slot-name: 'production'
publish-profile: ${{ secrets.AzureAppService_PublishProfile_af0528bd03f643298d541dab7b23e028 }}
images: 'scoopdevcontainerregistry.azurecr.io/${{ secrets.AzureAppService_ContainerUsername_7bb27a47a29b4c55a0946cf147f2b74a }}/feedback:${{ github.sha }}'

You should see docker/login-action@v2 with username and password setup for you. As well as the url and the publish-profile (credentials to publish), and the docker image you want to push. It will tag any commit made from a push with the github commit identifier github.sha

8. The only thing we need to do now is set any env vars we want and create a Dockerfile.

I updated the workflow yml & I created env section with any environmental variables my code base needed. I then added build-args to the docker section to pass these variables in. I also removed username as to where the image was being stored and deployed from.

# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
# More GitHub Actions for Azure: https://github.com/Azure/actions

name: Build and deploy container app to Azure Web App - facets-feedback-docker

on:
push:
branches:
- main
workflow_dispatch:

env:
NODE_VERSION: "18"
PORT: ${{ vars.PORT_DEV }}
NEXT_PUBLIC_BACKEND_URL: ${{ vars.NEXT_PUBLIC_BACKEND_URL }}

jobs:
build:
runs-on: "ubuntu-latest"

steps:
- uses: actions/checkout@v2

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

- name: Log in to registry
uses: docker/login-action@v2
with:
registry: https://scoopdevcontainerregistry.azurecr.io/
username: ${{ secrets.AzureAppService_ContainerUsername_7bb27a47a29b4c55a0946cf147f2b74a }}
password: ${{ secrets.AzureAppService_ContainerPassword_487e2a7c48c1428d9aee7922945def04 }}

- name: Build and push container image to registry
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
scoopdevcontainerregistry.azurecr.io/feedback:${{ github.sha }}
scoopdevcontainerregistry.azurecr.io/feedback:latest
file: ./Dockerfile
build-args: |
PORT=${{ env.PORT }}
NEXT_PUBLIC_BACKEND_URL=${{ env.NEXT_PUBLIC_BACKEND_URL }}

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@v2
with:
app-name: "facets-feedback-docker"
slot-name: "production"
publish-profile: ${{ secrets.AzureAppService_PublishProfile_af0528bd03f643298d541dab7b23e028 }}
images: "scoopdevcontainerregistry.azurecr.io/feedback:${{ github.sha }}"

9. In my Dockerfile set at the root of my codebase I passed in my build arguments. I echo my variables so I could see if they were passing through, but I recommend not printing sensitive data.

Read: https://nextjs.org/docs/pages/building-your-application/deploying to see what you need to deploy

make sure your next.config.js contains output:standalone

module.exports = {
output: 'standalone',
}

In my code base I used yarn instead of npm, therefore I needed to COPY yarn.lock instead of package-lock.json.

FROM node:18-alpine AS base

# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi


# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN yarn build

# If using npm comment out above and use below instead
# RUN npm run build

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1

# Build the application
ARG NEXT_PUBLIC_BACKEND_URL

# Print environment variables during build
RUN echo "NEXT_PUBLIC_BACKEND_URL: $NEXT_PUBLIC_BACKEND_URL"

# Set environment variables
ENV NEXT_PUBLIC_BACKEND_URL=$NEXT_PUBLIC_BACKEND_URL


RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000
# set hostname to localhost
ENV HOSTNAME "0.0.0.0"

# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD ["node", "server.js"]

10. Commit and push your changes. Follow the success of the build under your repo’s actions. If anything fails in the actions build you can click in and debug the logs.

11. If everything is successful you can test if the url works. If you need to debug anything head back to the Deployment Center and read through the logs.

--

--