ASP.NET Core: npm error when generating a Docker image of a project created with the React template
I have seen this problem with a project generated from the React SPA template, but maybe it can be applied to other SPA templates (such as Angular).
The error can be reproduced very easily. From an empty directory you can create a reactive SPA:
dotnet new react --name testspa dotnet new sln --name testspa dotnet sln add testspa\testspa.csproj
With that we have the project “testspa.csproj” and a Visual Studio solution (testspa.sln) to open it with Visual Studio and that this will generate the Dockerfile. To do this open the solution with Visual Studio and use the option “Add -> Docker Support” for Visual Studio to create the Dockerfile:
Add docket support in visual studio
That will create us the standard “Dockerfile”:
FROM microsoft/dotnet:2.2-aspnetcore-runtime AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM microsoft/dotnet:2.2-sdk AS build WORKDIR /src COPY ["testspa/testspa.csproj", "testspa/"] RUN dotnet restore "testspa/testspa.csproj" COPY . . WORKDIR "/src/testspa" RUN dotnet build "testspa.csproj" -c Release -o /app FROM build AS publish RUN dotnet publish "testspa.csproj" -c Release -o /app FROM base AS final WORKDIR /app COPY --from=publish /app . ENTRYPOINT ["dotnet", "testspa.dll"]
A priori it seems all right, and that is that this multistage scheme is “generally correct” for the vast majority of netcore projects:
- Use the SDK image to execute a “dotnet restore” and a “dotnet build”
- Then he makes the “dotnet publish” in a new stage
- Finally, copy the publish result into an image that starts from the runtime
- The problem is that … it does not work . You can check it yourself by launching the following command:
The problem is that … it does not work. You can check it yourself by launching the following command:
docker build -t testspa -f testspa\Dockerfile .
This command must be launched from the root directory (the directory where the sln file is, not the csproj file). That’s because the Dockerfile generated by VS takes as a context of build the directory where the solution is, not each of the projects. It is not something that I personally like, but VS does it because it is the only way to generate images of projects that have references to other projects in the solution.
Well … after a while the build will burst with that error:
Step 11/17 : RUN dotnet build "testspa.csproj" -c Release -o /app ---> Running in eccd6da4ab29 Microsoft (R) Build Engine version 15.9.20+g88f5fadfbe for .NET Core Copyright (C) Microsoft Corporation. All rights reserved. Restore completed in 795.87 ms for /src/testspa/testspa.csproj. The command '/bin/sh -c dotnet build "testspa.csproj" -c Release -o /app' returned a non-zero code: 137
The reason for the failure? Well, the netcore SDK image does not contain nodejs . And why do we need nodejs? Well, because the person who generated the project template thought it was a good idea to publish the project (with “dotnet publish”) to execute the npm sentences needed to generate the javascript bundles . And that’s why we have the following in the csproj file :
<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" /> <!-- Include the newly-built files in the publish output --> <ItemGroup> <DistFiles Include="$(SpaRoot)build\**" /> <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)"> <RelativePath>%(DistFiles.Identity)</RelativePath> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> </ResolvedFileToPublish> </ItemGroup> </Target>
This task is launched in the publication process (not build) and observe how the “npm install” and the “npm run build” are executed, which are the tasks necessary to build the JavaScript bundles .
Therefore in your machine a “dotnet publish” will work , since you will have no installed, but when that is executed in the Docker image of the SDK, that does not work because nodejs does not exist.
How can we solve it? Luckily the solution is quite easy. First we edit the csproj file so that this task is executed only if a certain environment variable does not exist :
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish" Condition=" '$(BuildingDocker)' == '' ">
Observe the ” Condition “, so that it is executed only if the BuildingDocker variable has some value (no matter what). In this way, in our local machine, everything will continue to work the same. Let’s go now for the Dockerfile. Beware of the Condition syntax that msbuild is very bloody and the white spaces must be respected.
The Dockerfile has to be modified a lot. On the one hand we must use the image of the netcore SDK to compile the project, but we also need the node image for the npm tasks. Finally, the results of both stages will be combined in one image with the netcore runtime:
FROM microsoft/dotnet:2.2-aspnetcore-runtime AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM node:10-alpine as build-node WORKDIR /ClientApp COPY testspa/ClientApp/package.json . COPY testspa/ClientApp/package-lock.json . RUN npm install COPY testspa/ClientApp/ . RUN npm run build FROM microsoft/dotnet:2.2-sdk AS build ENV BuildingDocker true WORKDIR /src COPY ["testspa/testspa.csproj", "testspa/"] RUN dotnet restore "testspa/testspa.csproj" COPY . . WORKDIR "/src/testspa" RUN dotnet build "testspa.csproj" -c Release -o /app FROM build AS publish RUN dotnet publish "testspa.csproj" -c Release -o /app FROM base AS final WORKDIR /app COPY --from=publish /app . COPY --from=build-node /ClientApp/build ./ClientApp/build ENTRYPOINT ["dotnet", "testspa.dll"]
In the stage build-node we use the Node image to execute the tasks “npm install” and “npm run build” that build the bundles and leave them in ClientApp / build.
Then in the stage build , we use the dotnet SDK to do the “dotnet build” and the “dotnet publish” but note how we use ENV to define the environment variable “BuildingDocker” with the value of true . Thanks to having defined that environment variable in the container, the PublishRunWebpack task of csproj is not executed , so we will not receive any error.
Finally in the last stage we combine the part generated by nodejs (ClientApp / build) and the part generated by dotnet publish (everything else) to have the whole application together.
Conclusions
Nothing new, but remember that the Dockerfiles created by Visual Studio are little more than a template that works in a large number of cases but there are several cases in which the generated files do not work correctly. Another thing you should remember is that in general, since you try to make the images as small as possible, they do one thing (take it as an SRP applied to the Docker images): that is why the image of the SDK of ASP.NET Core has no nodejs. In cases where you combine several tools for the construction of a project, in Docker what you do is combine N images in N stages and finally add the results.