Multi-stage Docker builds for ASP.NET Core 5
I am building an app with .NET 5 and ASP.NET Core 5. And, I want to run it on a cloud provider. Since .NET 5 isn't widely adopted yet (and probably won't be since it's not an LTS version), that means the easiest path to deployment is a Docker image.
That part isn't hard. When you create a new .NET project, you get a multi-stage Dockerfile for free (well, as long as you select that option). But, I want to use vanilla Node.js build options for processing and bundling my CSS and JS files. The common solutions I've seen for Docker + Node + .NET involve starting with the base .NET image and then using apt-get
to install Node. I didn't want to go down that path if I could help it. I don't know why, but the idea of modifying the Docker image outside of just building stuff offended my aesthetic.
What I've learned so far is that you can start with a .NET base image, then layer Node on that, build your app, and then copy out the final assets.
FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base
WORKDIR /app
FROM node:lts-buster-slim AS node_base
FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build
COPY --from=node_base . .
WORKDIR /src
COPY ["MyDotNetApp/MyDotNetApp.csproj", "MyDotNetApp/"]
RUN dotnet restore "MyDotNetApp/MyDotNetApp.csproj"
WORKDIR "/src/MyDotNetApp/"
COPY "MyDotNetApp/." .
ENV NODE_ENV=production
RUN npm ci
RUN npm run build
RUN dotnet build "MyDotNetApp.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "MyDotNetApp.csproj" -c Release -o /app/publish
FROM base AS final
EXPOSE 8080
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyDotNetApp.dll"]
You can see how everything fits together here where I am building SharpStatusApp.
The core bits:
FROM node:lts-buster-slim AS node_base
FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build
COPY --from=node_base . .
We're going to pull down Node's LTS image built on Buster-Slim and Microsoft's .NET 5 SDK image built on Buster-Slim. Then we'll copy the contents of the Node image into the .NET image. For me, this means I get the Node bits I need from an official image rather than using apt-get
to figure out and install my own dependencies.
On to the build part:
ENV NODE_ENV=production
RUN npm ci
RUN npm run build
RUN dotnet build "MyDotNetApp.csproj" -c Release -o /app/build
PurgeCSS will automatically tree-shake the final CSS for you if it's in production mode. So, I default the NODE_ENV
to production. This will also help with any other tooling I bring in later that might make different choices based on build environment.
You should use npm ci
on your build server rather than npm install
as ci
will use your package-lock.json
file to grab versions... which gives you a bit of confidence that you won't accidentally rev a version that you haven't tried locally yet.
Then it's a matter of running my npm script with npm run build
and compiling my .NET app with dotnet build
then we bundle things up and
get a svelte(?) final image with just our assets and runtime and none of the build tools or artifacts.
Wrapping Up
Including a Node.js Docker image as a base part of your build might be old hat, but I hadn't seen any examples of it. I wanted to document what I learned in case it helped anyone else down the road. I, personally, think it's a pretty clean way to assemble an app for deployment.
If you have any questions, comments or complaints, you can always DM or skeet me @hyrmn