Building Asp.Net Core Applications With an Angular Frontend Using Docker
By: Ashwin Kumar
Tags: Angular, Docker, Dotnet Core, Visual Studio 2019, Aspnetcore

Running .Net Core applications with a frontend SPA framework like Angular or React is now as simple as using one of the built-in templates in Visual Studio. Visual Studio also provides excellent docker support but there can be a few gotchas along the way that we need to be aware of.

In this blog post, we’ll be looking at building an ASP.Net Core application with an Angular SPA for the frontend and creating a ready-to-publish docker image out of it. The first thing we need is to create the project.

In Visual Studio, create a new project using the Asp.Net Core Web Application template and click next. Give the project a name and click create. On the next step, select Angular from the list of templates and click create.

Run the project by hitting F5. You should see the application open with the ‘hello world’ page.

Now to add docker support, we can right click the project and click on docker support under the add menu.

This will create a docker file in the project which looks like this,

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base

WORKDIR /app

EXPOSE 80

EXPOSE 443 FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build

WORKDIR /src

COPY ["DotNetAngular.csproj", ""]

RUN dotnet restore "./DotNetAngular.csproj"

COPY . .

WORKDIR "/src/."

RUN dotnet build "DotNetAngular.csproj" -c Release -o /app/build FROM build AS publish

RUN dotnet publish "DotNetAngular.csproj" -c Release -o /app/publish FROM base AS final

WORKDIR /app

COPY --from=publish /app/publish .

ENTRYPOINT ["dotnet", "DotNetAngular.dll"]

Taking a closer look at the sections, we see a multi-stage docker build process with the first section showing a base image (aspnet:3.1-buster-slim) which will contain our application,

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base

WORKDIR /app

EXPOSE 80

EXPOSE 443

The next section shows another image (sdk:3.1-buster) which is used to build our application,

FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build

WORKDIR /src

COPY ["DotNetAngular.csproj", ""]

RUN dotnet restore "./DotNetAngular.csproj"

COPY . .

WORKDIR "/src/."

RUN dotnet build "DotNetAngular.csproj" -c Release -o /app/build FROM build AS publish

RUN dotnet publish "DotNetAngular.csproj" -c Release -o /app/publish

Once the build process is complete, the contents of our application is copied into base, ending the process with an image containing our application.

Lets try to build this image that is ready to publish with docker. On a terminal window, navigate to the root of your project and try to build the image,

docker build -t dotnetangular .

Ooops! Errors!

We can see the publish process failed with an error when trying to do an npm install,

/bin/sh: 2: /tmp/tmpa036048c83bb44a0a2d9834ce6d641b7.exec.cmd: npm: not found

/src/DotNetAngular.csproj(42,5): error MSB3073: The command "npm install" exited with code 127.

The command '/bin/sh -c dotnet publish "DotNetAngular.csproj" -c Release -o /app/publish' returned a non-zero code: 1

Where is the npm install coming from? Upon editing the project file, the target for publish reveals the exact set of commands that are run to build the angular project,

<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">

<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->

<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />

<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build -- --prod" />

<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build:ssr -- --prod" Condition=" '$(BuildServerSideRenderer)' == 'true' " /> <!-- Include the newly-built files in the publish output -->

<ItemGroup>

<DistFiles Include="$(SpaRoot)dist\**; $(SpaRoot)dist-server\**" />

<DistFiles Include="$(SpaRoot)node_modules\**" Condition="'$(BuildServerSideRenderer)' == 'true'" />

<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">

<RelativePath>%(DistFiles.Identity)</RelativePath>

<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>

<ExcludeFromSingleFile>true</ExcludeFromSingleFile>

</ResolvedFileToPublish>

</ItemGroup>

</Target>

The target named ‘PublishRunWebpack’ is attempting to build the angular project, and the reason for failure is that it cannot find the npm command in the build container since the build image does not come with nodejs installed.

A simple fix would just be to install nodejs in the build container before starting the dotnet build process. To do this, edit the docker file and add the lines to install nodejs,



# Install NodeJs

RUN apt-get update && \

apt-get install -y wget && \

apt-get install -y gnupg2 && \

wget -qO- https://deb.nodesource.com/setup_12.x | bash - && \

apt-get install -y build-essential nodejs

# End Install

WORKDIR /src

COPY ["DotNetAngular.csproj", ""]

RUN dotnet restore "./DotNetAngular.csproj"

COPY . .

WORKDIR "/src/."

RUN dotnet build "DotNetAngular.csproj" -c Release -o /app/build FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS buildWORKDIR /srcCOPY ["DotNetAngular.csproj", ""]RUN dotnet restore "./DotNetAngular.csproj"COPY . .WORKDIR "/src/."RUN dotnet build "DotNetAngular.csproj" -c Release -o /app/build

Note: The node js version is set to 12.x but you can change this to match the nodejs version that you need for your project.

Run the docker build command again and the build should go through successfully.

Although this is working, we can make the process run faster by deploying an image of the build container with nodejs installed in it. This can be made available in your container registry or on docker hub, and we can then use this image in our docker file instead of the official image. I already have a version of this deployed to my docker hub here (Github). Another good addition would be to run the unit tests before the publish step and to add the — no-cache option to the dotnet restore command since docker will take care of caching layers. (Thanks Richard Collette for the tip)

To use it, revert the docker file build section to use this image and add the step to run unit tests, like so,

FROM ashwin027/dotnet-3.1-buster-node:latest AS build

WORKDIR /src

COPY ["DotNetAngular.csproj", ""]

RUN dotnet restore --no-cache "./DotNetAngular.csproj"

COPY . .

WORKDIR "/src/."

RUN dotnet build "DotNetAngular.csproj" -c Release -o /app/build FROM build AS publish

RUN dotnet test "DotNetAngular.csproj" --configuration Release --no-restore

RUN dotnet publish "DotNetAngular.csproj" -c Release -o /app/publish

Lets start a container based on this new image,

docker run -p 8001:80 -d dotnetangular

Open the app at http://localhost:8001 and you should see the app load successfully.

If you have further questions on the topic, feedback on the article or just want to say hi you can hit me up on twitter or linkedin.