[ASP.NET] Publishing with Docker
Publishing a web application with containerization is currently one of the simplest ways to allow an easily scalable infrastructure.
What is a container
A container is a package that contains all the environment configurations and dependencies necessary to run a software, including OS packages, binaries, language libraries, source code, etc.
As a concept and a technology, containerization is not new, and it has transformed through the years, gaining much more popularity with the release of Docker in 2013.
Docker
Docker is both the name of a technology and the name of a company. Most of this article talks about the technology, not the company.
Docker Inc (company) is currently the main maintainer of the Docker technology and offers a set of products and services around containerization, some of which are proprietary and non-free.
Docker (technology) is an open source containerization technology. The code is available on a GitHub repository.
Docker containers do not contain an entire operating system inside of them. They share the same Linux kernel running on the OS of the hosting machine and run additional dependencies in isolation.
Therefore, Docker containers are not meant to be run directly on a Windows OS. To run Docker on Windows, some form of Linux-Windows communication is required, such as WSL or virtualization.
Docker Desktop for Windows abstracts away that Linux-Windows communication process, allowing Windows to run containers as easily as a Linux OS.
Note that Docker Desktop for Windows has a licensing fee for commercial usage. Always check the licensing of each product to understand its limitations.
Components of containerization with Docker
Docker Image: A template that has all the data and configuration required to run a container on a computer. It can be hosted or shared on specialized repositories, known as container registries.
Docker Container: A container instance, created based on a Docker Image.
Docker Daemon: A service running on top of the OS and is responsible for keeping the containers running. It is also responsible for pushing and pulling images from registries, creating and managing container instances based on container images, and bridging the computer's network ports with the container's ports.
Why is it useful?
Containerization allows software to be distributed alongside the environment configuration. With containers, there is no need to install dependencies or reconfigure the operating system for each piece of software running on that machine.
This allows for easier scaling because a single server with Docker can deploy multiple containers using the same image. Cloud platforms also offer PaaS options, such as Azure Container Apps, to run containers without worrying about servers, and offering user-friendly auto-scaling options.
It also allows developers to run instances of software without requiring the installation of the software and its dependencies directly onto the developer's workstation, avoiding conflicts between dependencies of different software. Since dependencies are contained inside the container, deleting the container will also remove all its files and dependencies, simplifying the uninstallation process.
Simplified setup
Inside the root folder of the project, add a file containing the Docker instructions. A file containing Docker instructions is called a Dockerfile. The default name for this file is Dockerfile (without a file extension), but you can specify a different file name in the build command.
To create container images on your own computer, it is necessary to install Docker. If the image is created automatically as part of a CI/CD process, it is not necessary to have Docker installed on the computer.
# Example of a Dockerfile for an ASP.NET using .NET 8.0:
# Base image of an operating system that can build the code
# For .NET, the SDK version mirrors the .NET version
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /source
# Copy solution and relevant project files, and restore dependencies before publishing
# Docker does not support globbing to copy and filter files recursively
# Each file must be copied individually
COPY MyProject.sln ./MyProject.sln
COPY src/MyProject.Domain/MyProject.Domain.csproj ./src/MyProject.Domain/MyProject.Domain.csproj
COPY src/MyProject.Infrastructure/MyProject.Infrastructure.csproj ./src/MyProject.Infrastructure/MyProject.Infrastructure.csproj
COPY src/MyProject.Web/MyProject.Web.csproj ./src/MyProject.Web/MyProject.Web.csproj
RUN dotnet restore
# After restore, copy all source files and build
COPY src/. ./src/
WORKDIR /source/src/MyProject.Web
RUN dotnet publish -c Release -o /app --no-restore
# Copy the files into a base image capable of running the app
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["dotnet", "MyProject.Web.dll"]
The root folder can also include a .dockerignore file, which will exclude certain files, filenames or paths from the build process. Note that Docker does not support parameterized .dockerignore files.
When building an image of a .NET project locally, it is recommended to ignore the bin and obj folders to avoid copying files that might include local path dependencies.
# Ignore folders
**/bin
**/obj
bin/
obj/
Building the container image
To build the image on a computer, run the following command:
docker build --progress=plain --file=Dockerfile -t <image_name> .
--progress=plain displays command output and errors to help debug the docker build process.
--file is optional when the Dockerfile has the file name 'Dockerfile.' If different images are being created from the same root folder, multiple Dockerfiles with different names may be required.
-t is used to specify the name or tag of the created Docker image.
Why restore before publishing
Docker uses a caching system based on checksums, which is generated based on the command text and the files contained in the image at the moment the command runs.
Starting with the second build, if the previous build has a command with the same checksum as the command about to be executed, Docker reuses the previously generated output instead of running the command from scratch. When the checksum differs, all subsequent commands will be re-executed.
By separating the process of restoring dependencies based only on the project files, the restore process will only run again if the project files change, for example, when adding or updating a dependency.
This way, Docker can often reuse the cached package restore result, greatly speeding up the process.
Common errors
The following error is caused because the local bin and obj folders were copied into the Docker image and contain references to the local machine. To fix this, create a .dockerignore file to ignore those local folders and let the publishing process within Docker create its own folders.
Note that this error is more likely to happen on a local computer. If this is happening in the cloud, it is possible that a developer committed the bin and obj folders to the repository.
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018: The "ResolvePackageAssets" task failed unexpectedly. [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018: NuGet.Packaging.Core.PackagingException: Unable to find fallback package folder 'C:Program Files (x86)Microsoft Visual StudioSharedNuGetPackages'. [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at NuGet.Packaging.FallbackPackagePathResolver..ctor(String userPackageFolder, IEnumerable`1 fallbackPackageFolders) [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at Microsoft.NET.Build.Tasks.NuGetPackageResolver.CreateResolver(IEnumerable`1 packageFolders) [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at Microsoft.NET.Build.Tasks.ResolvePackageAssets.CacheWriter..ctor(ResolvePackageAssets task) [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at Microsoft.NET.Build.Tasks.ResolvePackageAssets.CacheReader.CreateReaderFromDisk(ResolvePackageAssets task, Byte[] settingsHash) [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at Microsoft.NET.Build.Tasks.ResolvePackageAssets.CacheReader..ctor(ResolvePackageAssets task) [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at Microsoft.NET.Build.Tasks.ResolvePackageAssets.ReadItemGroups() [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at Microsoft.NET.Build.Tasks.ResolvePackageAssets.ExecuteCore() [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at Microsoft.NET.Build.Tasks.TaskBase.Execute() [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at Microsoft.Build.BackEnd.TaskExecutionHost.Execute() [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at Microsoft.Build.BackEnd.TaskBuilder.ExecuteInstantiatedTask(TaskExecutionHost taskExecutionHost, TaskLoggingContext taskLoggingContext, TaskHost taskHost, ItemBucket bucket, TaskExecutionMode howToExecuteTask) [/source/src/MyProject.Web/MyProject.Web.csproj]