Docker containers are a new way of deploying and rapid scaling of applications in a simple and most configurable manner. In the era of high availability and scalability requirements for applications, docker containers provide way of creating customized application environments with all necessary prerequisites in place. Similar to many application stacks which can be "dockerized" into containers, aspnetcore is also provided with necessary docker development and runtime "images" which are needed for creating containers.
What are Containers?
In a nutshell, a container is an encapsulated environment with necessary runtime and other configurations in place for applications to be able run independently as if running in a real machine. It is a virtual copy created from a real host machine, with necessary configurations in place facilitating easy extensibility.
In order to create a container image, we would require a "base image" which shall be used for putting in the application binaries along with settings about how to start and how to function.
For an aspnetcore application, building and deploying an application is done in below steps:
In the developer environment which we call the build environment:
In the runtime environment where the application runs until stopped:
These steps are translated into docker language in terms of a Dockerfile, which is used to build containerized images for any application using a base image.
For dotnetcore, we are provided with two base images: one is an sdk image without any runtime environment. the second is a runtime environment without sdk. In order to publish and deploy an aspnetcore application we would make use of these two images to create two build "steps".
Let's begin by creating a Dockerfile (filename is Dockerfile without any extension) in the root of our application directory, which contains the below instructions to build - publish - run.
# Step 1 - The Build Environment # #Base Image for Build - dotnetcore SDK Image FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build WORKDIR /app #Copy the csproj file first to restore required packages COPY *.csproj . COPY *.config . RUN dotnet restore --disable-parallel --configfile ./nuget.config #Copy all the source code into the Build Container COPY . . # Run dotnet publish in the Build Container # Generates output available in /app/out # Since the current directory is /app RUN dotnet publish -c Release -o out # Step 1 Ends - The binaries are generated #
One can observe that we're pretty much using the same dotnet commands which are usually used to restore and publish an application.
Sometimes during "dotnet restore" build step, we might encounter network issues which can result in error such as:
restore: Received an unexpected EOF or 0 bytes from the transport stream
which occurs when we use "dotnet restore" without any extra arguments. To fix this, we'd specify a nuget config file within the application root directory, like below:
<?xml version="1.0" encoding="utf-8"?> <configuration> <packageSources> <add key="NuGet" value="https://api.nuget.org/v3/index.json" /> </packageSources> </configuration>
Additionally, we force the nuget to disable parallel downloads, by the flag --disable-parallel. These two extra parameters solve the error but can delay the restore process longer than usual.
dotnet restore --disable-parallel --configfile ./nuget.config
In the second build step, we fetch the binaries generated from previous step which is aliased as "build" denoted in the first instruction - FROM. This makes the docker runtime look for specified directory to copy from the "build" directory. Other steps are pretty much self explanatory.
# Step 2 - Continuing from the End of Step 1 # # Second Stage - Pick an Image with only dotnetcore Runtime FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS runtime # Set the Directory as /app # All consecutive operations happen under /app WORKDIR /app # Copy the dlls generated under /app/out of the previous step # With alias build onto the current directory # Which is /app in runtime COPY --from=build /app/out . # Set the Entrypoint for the Container # Entrypoint is for executables (such as exe, dll) # Which cannot be overriden by run command # or docker-compose ENTRYPOINT ["dotnet", "OidcApp.dll"]
The ENTRYPOINT in this case is different from the CMD instruction we generally use in Dockerfile . An ENTRYPOINT indicates that the specified binary is an executable (dll, exe as such) and makes sure that this startup command can't be overriden in a docker run command.
docker build .
dot (.) specifies the location of the Dockerfile. Generally we run the command in the same directory where the Dockerfile is present.
This builds a docker container for us. Then we run the created container as:
docker run -p 80:80 <container_id>
The other way is by means of a docker-compose file. A Docker-Compose file is a yaml configuration file, which helps us in simplifying docker run command execution through a yaml script. In this script we specify all the parameters and configurations which otherwise need to be passed via the docker run command.
--- docker-compose.yaml --- version: "3" services: webapp: build: . ports: - 80:80
And to finish things up, we'd run the below command which builds and runs a container with our application running within.
> docker-compose up
When we're running docker container with a port configuration, we might run into error as below:
"docker: Error response from daemon: Ports are not available: unable to list exposed ports: Get http://unix/forwards/list: open \\.\pipe\dockerBackendApiServer: The system cannot find the file specified"
It seems that this error occurs when we're not running the docker commands in an elevated prevliges. To solve this we can just run our commands in a Command Prompt with an administrator prevliges or a sudo user. Or we can have the docker desktop run in administrator prevliges.
Once we're all done, we can test our application by navigating to http://localhost:80 which is the port we have specified in our port configuration, that is translated by the docker to reach the port 80 within the container where the application runs.