Improvements on ASP.NET Core deployments on Zeit's now.sh and making small container images
Back in March of 2017 I blogged about Zeit and their cool deployment system "now." Zeit will take any folder and deploy it to the web easily. Better yet if you have a Dockerfile in that folder as Zeit will just use that for the deployment.
Zeit's free Open Source account has a limit of 100 megs for the resulting image, and with the right Dockerfile started ASP.NET Core apps are less than 77 megs. You just need to be smart about a few things. Additionally, it's running in a somewhat constrained environment so ASP.NET's assumptions around FileWatchers can occasionally cause you to see errors like
at System.IO.FileSystemWatcher.StartRaisingEvents()
Unhandled Exception: System.IO.IOException:
The configured user limit (8192) on the number of inotify instances has been reached.
at System.IO.FileSystemWatcher.StartRaisingEventsIfNotDisposed(
While this environment variable is set by default for the "FROM microsoft/dotnet:2.1-sdk" Dockerfile, it's not set at runtime. That's dependent on your environment.
Here's my Dockerfile for a simple project called SuperZeit. Note that the project is structured with a SLN file, which I recommend.
Let me call our a few things.
- First, we're doing a Multi-stage build here.
- The SDK is large. You don't want to deploy the compiler to your runtime image!
- Second, the first copy commands just copy the sln and the csproj.
- You don't need the source code to do a dotnet restore! (Did you know that?)
- Not deploying source means that your docker builds will be MUCH faster as Docker will cache the steps and only regenerate things that change. Docker will only run dotnet restore again if the solution or project files change. Not the source.
- Third, we are using the aspnetcore-runtime image here. Not the dotnetcore one.
- That means this image includes the binaries for .NET Core and ASP.NET Core. We don't need or want to include them again.
- If you were doing a publish with a the -r switch, you'd be doing a self-contained build/publish. You'd end up copying TWO .NET Core runtimes into a container! That'll cost you another 50-60 megs and it's just wasteful.
- If you want to learn more, go explore the very good examples on the .NET Docker Repro on GitHub https://github.com/dotnet/dotnet-docker/tree/master/samples
- Optimizing Container Size
- .NET Core Alpine Docker Sample - This sample builds, tests, and runs an application using Alpine.
- .NET Core self-contained Sample - This sample builds and runs an application as a self-contained application.
- Finally, since some container systems like Zeit have modest settings for inotify instances (to avoid abuse, plus most folks don't use them as often as .NET Core does) you'll want to set ENV DOTNET_USE_POLLING_FILE_WATCHER=true which I do in the runtime image.
So starting from this Dockerfile:
FROM microsoft/dotnet:2.1-sdk-alpine AS build
WORKDIR /app
# copy csproj and restore as distinct layers
COPY *.sln .
COPY superzeit/*.csproj ./superzeit/
RUN dotnet restore
# copy everything else and build app
COPY . .
WORKDIR /app/superzeit
RUN dotnet build
FROM build AS publish
WORKDIR /app/superzeit
RUN dotnet publish -c Release -o out
FROM microsoft/dotnet:2.1-aspnetcore-runtime-alpine AS runtime
ENV DOTNET_USE_POLLING_FILE_WATCHER=true
WORKDIR /app
COPY --from=publish /app/superzeit/out ./
ENTRYPOINT ["dotnet", "superzeit.dll"]
Remember the layers of the Docker images, as if they were a call stack:
- Your app's files
- ASP.NET Core Runtime
- .NET Core Runtime
- .NET Core native dependencies (OS specific)
- OS image (Alpine, Ubuntu, etc)
For my little app I end up with a 76.8 meg image. If want I can add the experimental .NET IL Trimmer. It won't make a difference with this app as it's already pretty simple but it could with a larger one.
BUT! What if we changed the layering to this?
- Your app's files along with a self-contained copy of ASP.NET Core and .NET Core
- .NET Core native dependencies (OS specific)
- OS image (Alpine, Ubuntu, etc)
Then we could do a self-Contained deployment and then trim the result! Richard Lander has a great dockerfile example.
See how he's doing the package addition with the dotnet CLI with "dotnet add package" and subsequent trim within the Dockerfile (as opposed to you adding it to your local development copy's csproj).
FROM microsoft/dotnet:2.1-sdk-alpine AS build
WORKDIR /app
# copy csproj and restore as distinct layers
COPY *.sln .
COPY nuget.config .
COPY superzeit/*.csproj ./superzeit/
RUN dotnet restore
# copy everything else and build app
COPY . .
WORKDIR /app/superzeit
RUN dotnet build
FROM build AS publish
WORKDIR /app/superzeit
# add IL Linker package
RUN dotnet add package ILLink.Tasks -v 0.1.5-preview-1841731 -s https://dotnet.myget.org/F/dotnet-core/api/v3/index.json
RUN dotnet publish -c Release -o out -r linux-musl-x64 /p:ShowLinkerSizeComparison=true
FROM microsoft/dotnet:2.1-runtime-deps-alpine AS runtime
ENV DOTNET_USE_POLLING_FILE_WATCHER=true
WORKDIR /app
COPY --from=publish /app/superzeit/out ./
ENTRYPOINT ["dotnet", "superzeit.dll"]
Now at this point, I'd want to see how small the IL Linker made my ultimate project. The goal is to be less than 75 megs. However, I think I've hit this bug so I will have to head to bed and check on it in the morning.
The project is at https://github.com/shanselman/superzeit and you can just clone and "docker build" and see the bug.
However, if you check the comments in the Docker file and just use the a "FROM microsoft/dotnet:2.1-aspnetcore-runtime-alpine AS runtime" it works fine. I just think I can get it even smaller than 75 megs.
Talk so you soon, Dear Reader! (I'll update this post when I find out about that bug...or perhaps my bug!)
UPDATE1 : The linker works with this Workaround. I need to set the property CrossGenDuringPublish
to false
in the project file.
- A standard ASP.NET Core "hello world" image ends up at around 75 megs on Zeit.
- A self-contained deployment with the runtime-deps images is about 52 megs.
- If you add trimming to that self-contained Alpine image the result is just 35 megs!
I'm making some headway but still hitting an inotify issue with FileSystemWatchers. More soon!
UPDATE2: After some bugs found and some hard work by our friends at Zeit it looks like the inotify issue in the sentence above has been fixed. Looks like it was a misconfiguration - which is great! I was worried there was a larger architectural issue but there isn't.
Sponsor: Preview the latest JetBrains Rider with its built-in spell checking, initial Blazor support, partial C# 7.3 support, enhanced debugger, C# Interactive, and a redesigned Solution Explorer.
About Scott
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.
About Newsletter
Comments are closed.