dotnet 6 was recently released as was Azure’s new container app in preview. I recently went through the process of creating and deploying a dotnet 6 Blazor web assembly (WASM) project to this new Azure Container App service. I hit a few bumps along the way and want to share with you what I learned.
Building the Container
This is where I spent a lot of time troubleshooting. Visual Studio provides a template for building your project into a container. However, some parts of this needs to be changed drastically. First, let’s look at the docker file that is generated in visual studio.
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["AzureContainerApp/AzureContainerApp.csproj", "AzureContainerApp/"]
RUN dotnet restore "AzureContainerApp/AzureContainerApp.csproj"
COPY . .
WORKDIR "/src/AzureContainerApp"
RUN dotnet build "AzureContainerApp.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "AzureContainerApp.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "AzureContainerApp.dll"]
Starting with the base image. Serving a WASM app is a static page, we need something to serve the index page. To serve the index page I used nginx. being that our base image is only used at the end, we can remove the base image from the docker file.
The build image from the dotnet 6 SDK appears to be lacking in some tooling. MSDN docs state to run dotnet workload install wasm-tools
in a command shell to install some of the required tooling. What I learned when I ran the dotnet publish command is that Emscripten SDK isn’t installed. A requirement for dotnet 6 applications (Tooling for ASP.NET Core Blazor | Microsoft Docs). So instead of installing the emsdk, I decided to use the emsdk as the build image and install dotnet 6 on top of that. Our publish image remains unchanged, and together, looks like this:
FROM emscripten/emsdk:3.0.1 as build
RUN wget https://packages.microsoft.com/config/ubuntu/21.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb; dpkg -i packages-microsoft-prod.deb;rm packages-microsoft-prod.deb
RUN apt-get update; apt-get install -y apt-transport-https && \
apt-get update && \
apt-get install -y dotnet-sdk-6.0
WORKDIR /src
RUN dotnet workload install wasm-tools
COPY ["AzureContainerApp/AzureContainerApp.csproj", "AzureContainerApp/"]
RUN dotnet restore "AzureContainerApp/AzureContainerApp.csproj"
COPY . .
WORKDIR "/src/AzureContainerApp"
RUN dotnet build "AzureContainerApp.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "AzureContainerApp.csproj" -c Release -o /app/publish/
Our final image was built from the dotnet asp base image. We deleted that image as it didn’t suit our needs. Instead, we’re going to use nginx as our final image. We’re going to expose port 80 and 443, set our working directory to the nginx’s html folder, copy our published wwwroot contents and copy over an nginx config. Our final image looks like this:
FROM nginx:alpine AS final
EXPOSE 80
EXPOSE 443
WORKDIR /usr/share/nginx/html
COPY --from=publish /app/publish/wwwroot .
COPY "AzureContainerApp/nginx.conf" /etc/nginx/nginx.conf
This is our docker file to build our dotnet 6 Blazor WASM container. Lets look at the nginx configuration:
events { }
http {
include mime.types;
server {
listen 80;
index index.html;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html =404;
}
}
}
This is a basic nginx configuration. One thing to note is line 11. WASM is a static site where all requests should be re-routed back to the index page. Because there is only and index page, any requests to any other page should be not found and thus, route back to the index.
Deploying the Container
So now that we have the dockerfile all shored up to build our image, lets go over how to host it. This project was to see the viability of Azure’s new preview feature, Container App, to host a blazor WASM container. Following MSDN docs (Quickstart: Deploy your first container app | Microsoft Docs) it is a simple process. Some setup is required though. A container registry is needed. The scripts below are assuming that we are using ACR with anonymous pulls. You also need to have the extension installed and open a new shell. My full build/deploy script looks like this:
# assumes registry has anonymous pull access
# assumes 'containerapp' az cli extension installed already (az extension add --source https://workerappscliextension.blob.core.windows.net/azure-cli-extension/containerapp-0.2.0-py2.py3-none-any.whl; az provider register --namespace Microsoft.Web)
# --- Docker Build/Publish --- #
$ErrorActionPreference = 'Stop'
$ACR_User = ""
$ACR_Pwd = ""
$ACR_Name = ""
$ACR_URL = "$ACR_Name.azurecr.io"
$container_name = ""
Set-Location "$PSScriptroot/src/azurecontainerapp"
docker build -t $container_name -f "Dockerfile" ..
docker login -u $ACR_User -p $ACR_Pwd $ACR_URL
$remoteImageName = "$ACR_URL/$($container_name):latest"
docker tag $container_name $remoteImageName
docker push $remoteImageName
# --- Azure Container App --- #
$resource_group = ""
$location="canadacentral"
$log_analytics_workspace=""
$CONTAINERAPPS_ENVIRONMENT=""
# if containerapp env (kubernetes environment) doesn't already exist run this:
# $LOG_ANALYTICS_WORKSPACE_CLIENT_ID=(az monitor log-analytics workspace show --query customerId -g $RESOURCE_GROUP -n $LOG_ANALYTICS_WORKSPACE --out tsv)
# $LOG_ANALYTICS_WORKSPACE_CLIENT_SECRET=(az monitor log-analytics workspace get-shared-keys --query primarySharedKey -g $RESOURCE_GROUP -n $LOG_ANALYTICS_WORKSPACE --out tsv)
# az containerapp env create `
# --name $CONTAINERAPPS_ENVIRONMENT `
# --resource-group $RESOURCE_GROUP `
# --logs-workspace-id $LOG_ANALYTICS_WORKSPACE_CLIENT_ID `
# --logs-workspace-key $LOG_ANALYTICS_WORKSPACE_CLIENT_SECRET `
# --location "$location"
Write-Host "Creating Container App"
az containerapp create `
--name "ContainerAppName" `
--resource-group $RESOURCE_GROUP `
--environment $CONTAINERAPPS_ENVIRONMENT `
--image $remoteImageName `
--target-port 80 `
--ingress 'external'
Write-Host "Finished creating container app"
Set-Location "$PSScriptRoot"
This script was a quick proof of concept so I commented out creating the container app environment. For your own script, I would recommend adding some logic on line 30 to switch if you are using an existing environment, or if a new one needs to be created. This script is also my build script, where the top portion builds my docker file. This script is located at the root of my project, beside my source folder, and my source folder contains my Visual Studio solution. The docker file we created lives beside my csproj file. This is important as the build script and docker file assumes this hierarchy.
Going through this piece by piece, skipping over the docker push we start with creating an environment. Azure Container Apps groups containers into environments. Logs and metrics are then tied to these environments. From there, the container app is created.
Creating the container app from the CLI is straight forward. Provide a name to be used as the resource display name, the resource group for it to be found in. Provide the environment, either the one created or one that already exists and the image. If your container is going to be public facing, include the ingress and target port parameters.
Closing
This is mostly a straightforward deployment. Surprisingly, most of my troubles came from, not the preview features, but the released, Microsoft provided tooling. A lot of other blogs talking about Blazor WASM in docker are for netcore 3 or 5. I found that those didn’t address the issues I encountered so I wanted to share my experience with you. One thing to note where you can probably avoid all this is to treat Blazor WASM as a static web site, with a single page. Hosting them on Azure Blob storage or AWS S3 buckets is probably the most ideal solution, unless your situation dictates otherwise.