Runar Ovesen Hjerpbakk

Software Philosopher

Build an ASP.Net Core App with React and Docker Hosting

I started a new project this week, a SPA written in React with an ASP.Net Core 2.1.1 backend hosted in a Docker container. This is the tale of how I got all those pieces working together.

The running code from this article can be seen on GitHub.

Creating a ASP.Net Core project with React from a dotnet template

Microsoft has helpfully created a template for this very scenario.

dotnet new react -o react-app
cd react-app

I created the app from the template and changed directory. From there I ran:

dotnet run

The result was a failed build with the following error:

error NU1605: Detected package downgrade: Microsoft.AspNetCore.SpaServices.Extensions from 2.1.1 to 2.1.0. Reference the package directly from the project to select a different version.

The .csproj-file contained these references:

<ItemGroup>
   <PackageReference Include="Microsoft.AspNetCore.App" />
   <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="2.1.0" />
</ItemGroup>

The Microsoft.AspNetCore.App meta-package is new from ASP.Net 2.1.1. This package should only be reference without a version number, thus needing no change when the version number is upped by Microsoft.

No such luck with Microsoft.AspNetCore.SpaServices.Extensions which only works with .Net Core version 2.1.0. I removed this reference:

<ItemGroup>
   <PackageReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

And tried dotnet run again:

Running React in an ASP.Net Core Docker-container

The app ran locally, now was the time to create a Docker container for testing and publishing.

Creating Dockerfile

Avid readers of this blog will know that I use multi-stage Dockerfiles for my containers. I’m basing these on the official dotnet images supplied by Microsoft.

You’ll also remember that we need a .dockerignore-file to prevent locally built items to be copied.

Creating a Dockerfile for building and running a regular ASP.Net Core 2.1.1 app is easy:

FROM microsoft/dotnet:2.1.301-sdk AS builder
WORKDIR /source

COPY *.csproj .
RUN dotnet restore

COPY ./ ./

RUN dotnet publish "./react-app.csproj" --output "./dist" --configuration Release --no-restore

FROM microsoft/dotnet:2.1.1-aspnetcore-runtime
WORKDIR /app
COPY --from=builder /source/dist .
EXPOSE 80
ENTRYPOINT ["dotnet", "react-app.dll"]

And .dockerignore is equally simple:

**/obj/
**/bin/

Building an image with the React-app from the template using this Dockerfile, resulted in:

docker build -t hjerpbakk/react-app .
react-app -> /source/bin/Release/netcoreapp2.1/react-app.Views.dll
  /bin/sh: 2: /tmp/tmp9dfbab16bdf54855b9b5da0e68821afa.exec.cmd: npm: not found
/source/react-app.csproj(34,5): error MSB3073: The command "npm install" exited with code 127.
The command '/bin/sh -c dotnet publish "./react-app.csproj" --output "./dist" --configuration Release --no-restore' returned a non-zero code: 1

I needed to install Node.js in order to build react. Thus, the Dockerfile needed to be:

FROM microsoft/dotnet:2.1.301-sdk AS builder
WORKDIR /source

RUN curl -sL https://deb.nodesource.com/setup_10.x |  bash -
RUN apt-get install -y nodejs

COPY *.csproj .
RUN dotnet restore

COPY ./ ./

RUN dotnet publish "./react-app.csproj" --output "./dist" --configuration Release --no-restore

FROM microsoft/dotnet:2.1.1-aspnetcore-runtime
WORKDIR /app
COPY --from=builder /source/dist .
EXPOSE 80
ENTRYPOINT ["dotnet", "react-app.dll"]

But the build did not succeed yet.

Getting Node.js to restore correct versions of NPM modules

With the updated Dockerfile, the command docker build -t hjerpbakk/react-app . gave the following error now:

  > react_app@0.1.0 build /source/ClientApp
  > react-scripts build
  
  sh: 1: react-scripts: not found
  npm ERR! file sh
  npm ERR! code ELIFECYCLE
  npm ERR! errno ENOENT
  npm ERR! syscall spawn
  npm ERR! react_app@0.1.0 build: react-scripts build
  npm ERR! spawn ENOENT
  npm ERR!
  npm ERR! Failed at the react_app@0.1.0 build script.
  npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
  
  npm ERR! A complete log of this run can be found in:
  npm ERR!     /root/.npm/_logs/2018-06-25T11_26_23_802Z-debug.log
/source/react-app.csproj(35,5): error MSB3073: The command "npm run build" exited with code 1.
The command '/bin/sh -c dotnet publish "./react-app.csproj" --output "./dist" --configuration Release --no-restore' returned a non-zero code: 1

This was not enough information for me to debug this issue. I needed to run commands using bash in the partially built container.

To do this, make a note of the last stage of the build process that succeeded. In this case, it was the dotnet publish "./react-app.csproj" --output "./dist" --configuration Release --no-restore command that failed. Scrolling up in the output, we find the id of the stage on which this command was run:

Bash could then run in the container using the following command:

docker run --rm -it 2d7680490d55 bash -il

This will start Bash on the partially built container using interactive mode. You don’t need to run Bash, all applications present on the container can be run in this way as per this answer on StackOverflow.

I read the logs from the npm install command and remembered that I forgot to exclude the locally built Node modules with .dockerignore. Thus, modules built for debug mode on macOS was used in production mode on Debian. I updated .dockerignore:

**/obj/
**/bin/
**/node_modules/

And finally docker build -t hjerpbakk/react-app . completed successfully!

The container could the be run with:

docker run -p 80:80 hjerpbakk/react-app

Conclusion

ASP.Net, React and Docker are great tools to use together. However, they are also moving targets in constant motion. Thus at the time of writing, the following was needed to go from Hello World! locally to Hello World! running in a Docker container:

  1. Remove reference added by the template that is no longer needed.
  2. The container image needed more tools than the image from Microsoft provided.
  3. Never include locally built files in the build context used by the Dockerfile. .dockerignore truly is your friend!