Docker for ASP.Net Core SPA projects
This time we look into how to create Docker for ASP.Net Core SPA projects.
Create SPA Project
When you create a netcore SPA project, either using VS or using dotnet new and some SPA templates as a react (dotnet new react ), a structure similar to the following is generated:
The “ClientApp” folder contains all the client code (javascript, CSS, and others) while the rest is the netcore code that is limited to “launch” the SPA.
In Startup.cs the UseSpa middleware is used, which is in charge of serving the file “index.html” and also allows the use of development servers. e.g. In the case of a reactive application, the code is:
app.UseSpa(spa => { spa.Options.SourcePath = "YourApp"; if (env.IsDevelopment()) { spa.UseReactDevelopmentServer(npmScript: "start"); } });
We basically tell you to look for the file “index.html” of the “ClientApp” directory and, only if we are in development, use the React development server.
This development server requires node, so, in development, we need to have NodeJs installed. It is not a problem, because if you develop a SPA with React, you will have NodeJs installed in all likelihood.
However, in production, there is no need to use Node, in this case, it is imperative that the bundles have been generated and are in ClientApp / build. If we are running using dotnet myspa.dll we must have generated the bundles manually: VS does not do it for us.
NPM Build
if we publish the project (using dotnet publish ) then the bundles are generated. That is due to the following lines of the csproj:
<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>
That defines a task of its own, called “PublishRunWebpack” · that will be executed when the application is published (not when compiling, when publishing). This task does three things:
- Run npm install
- Run npm run build
- Add the generated files in ClientApp \ build \ to the publication result so that they are copied to the publication package (otherwise these files would be ignored).
This works perfectly but requires that the machine that launches the dotnet publish have node installed. In certain environments, there are no problems (eg in the case of generating a build with VSTS the agent has the sdk of netcore and node so everything will work), but in Docker, we will have frictions.
The reason is that the image of the netcore SDK, which we use to compile, has no pre-installed nodejs (before netcore 2.x it had it). It’s a good decision: we want small images and nodejs is not necessary for the vast majority of netcore projects. One option you can try is to install nodejs in the netcore sdk image . In fact, I have a gist that shows how to do it , but the reality is that it is not necessary . It is much better and easier to use a multi-stage build with two steps of build: in the first, we use nodejs to create the bundles, in the second netcore sdk to compile and finally we combine everything in a final stage of execution:
FRоM nоde:10-аlpine аs build-nоde WоRKDIR /Clientаpp CоPY Clientаpp/pаckаge.jsоn . CоPY Clientаpp/pаckаge-lоck.jsоn . RUN npm instаll CоPY Clientаpp/ . RUN npm run build FRоM micrоsоft/dоtnet:2.2.100-preview3-sdk-stretch аs build-net WоRKDIR /аpp CоPY *.csprоj . RUN dоtnet restоre CоPY . . RUN dоtnet build RUN dоtnet publish -о /myweb FRоM micrоsоft/dоtnet:2.2.0-preview3-аspnetcоre-runtime WоRKDIR /web CоPY --frоm=build-net /ttweb . CоPY --frоm=build-nоde /Clientаpp/build ./Clientаpp/build ENTRYPоINT [ "dоtnet","myspа.dll" ]
Observe how in the “build-node” stage we use npm install and npm run build to generate the required bundles. Then in the stage “build-net” we use dotnet build and dotnet publish to publish the web and in the stage “runtime” we start from the image with the asp.net core runtime and copy the bundles on the one hand and on the other hand the compiled netcore code.
And voila! Ready. Simple, right? Yes, except that this as it is does NOT WORK.
The reason? Well, what I have said before: the machine that makes the “dotnet publish” requires nodejs and the microsoft / dotnet image does not incorporate it. Luckily the solution is super simple, just two steps:
First, edit the csproj and make the PublishRunWebpack task conditional on the value of a variable, p. ex. called BuildingDocker :
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish" Condition=" '$(BuildingDocker)' == '' >
We have added the Condition attribute. Thus in this way only if BuildindDocker is not defined this task will be executed. Otherwise, we will skip this task, which is exactly what we want in Docker’s case.
So, of course, the second step is to define that environment variable called BuildingDocker in the “build-net” stage:
FRоM micrоsоft/dоtnet:2.2.100-preview3-sdk-stretch аs build-net ENV BuildingDоcker true WоRKDIR /аpp CоPY *.csprоj . RUN dоtnet restоre CоPY . . RUN dоtnet build RUN dоtnet publish -о /ttweb
Observe the use of ENV to define environment stackable BuildingDocker (beware that the value is not relevant, only that it is defined, so even if you define it with false the task will not be executed).
And that’s it, this is how to create Docker for ASP.Net Core SPA projects! Now you can build your Docker images of SPA applications, without any problem!