Creating an ASP.Net Core app with a React frontend 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:
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.
.csproj-file contained these references:
<ItemGroup> <PackageReference Include="Microsoft.AspNetCore.App" /> <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="2.1.0" /> </ItemGroup>
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>
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.
You’ll also remember that we need a
.dockerignore-file to prevent locally built items to be copied.
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"]
.dockerignore is equally simple:
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:
> [email protected] 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! [email protected] build: react-scripts build npm ERR! spawn ENOENT npm ERR! npm ERR! Failed at the [email protected] 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
**/obj/ **/bin/ **/node_modules/
docker build -t hjerpbakk/react-app . completed successfully!
The container could the be run with:
docker run -p 80:80 hjerpbakk/react-app
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:
- Remove reference added by the template that is no longer needed.
- The container image needed more tools than the image from Microsoft provided.
- Never include locally built files in the build context used by the Dockerfile. .dockerignore truly is your friend!