Creating and running an ASP.NET Core website on Docker using the latest .NET Core framework is fun. Let's learn how.
Photo by Guillaume Bolduc on Unsplash |
- Create and run ASP.NET Core 3.1 website
- Build your first container
- Run your website as a local container
- Understand the basic commands
- Troubleshooting
Requirements
For this post, I'll ask you to make sure that you have the following requirements installed:- .NET Core 3.1
- Docker Desktop for Windows or Mac
Linux users should be able to follow along assuming they have .NET Core and Docker installed. Podman, a very competent alternative to Docker should work too.
Containers in .NET world
So what's the state of containers in the ASP.NET world? Microsoft started late on the game but since .NET Core 2.2 we started seeing steady increases in container adoption. The ecosystem also matured. If you look at their official ASP.NET sample app on GitHub, you'll can now run your images on Debian (default), Alpine, Ubuntu and Windows Nano Server.Describing our Project
The version we'll use (3.1) is the latest LTS before .NET Framework and .NET Core merge as .NET 5. That's excellent news for slower teams as they'll be able to catch up. However, don't sit and wait, it's worth understanding how containers, microservices and orchestration technologies work so you're able to help your team in the future.For our project we'll use two images: the official .NET Core 3.1 SDK to build our project and the official ASP.NET Core 3.1 to run it. As always, our project will be a simple ASP.NET MVC Core web app scaffolded from the dotnet CLI.
Downloading the .NET Core Docker SDK
This step is optional but if you're super excited and want to get your hands in the code already, consider running the below command. Docker will pull dotnet's Docker SDK and store on your local repository.
docker pull mcr.microsoft.com/dotnet/core/sdk:3.1
C:\src\>docker pull mcr.microsoft.com/dotnet/core/sdk:3.1
3.1: Pulling from dotnet/core/aspnet
c499e6d256d6: Pull complete
251bcd0af921: Pull complete
852994ba072a: Pull complete
f64c6405f94b: Pull complete
9347e53e1c3a: Pull complete
Digest: sha256:31355469835e6df7538dbf5a4100c095338b51cbe52154aa23ae79d87585d404
Status: Downloaded newer image for mcr.microsoft.com/dotnet/core/aspnet:3.1
mcr.microsoft.com/dotnet/core/aspnet:3.1
C:\src\>docker pull mcr.microsoft.com/dotnet/core/sdk:3.1
3.1: Pulling from dotnet/core/aspnet
c499e6d256d6: Pull complete
251bcd0af921: Pull complete
852994ba072a: Pull complete
f64c6405f94b: Pull complete
9347e53e1c3a: Pull complete
Digest: sha256:31355469835e6df7538dbf5a4100c095338b51cbe52154aa23ae79d87585d404
Status: Downloaded newer image for mcr.microsoft.com/dotnet/core/aspnet:3.1
mcr.microsoft.com/dotnet/core/aspnet:3.1
This is a good test to see if your Docker Desktop is correctly installed. As we'll see when we build our image, Docker skips repulling the image from the remote host if it exists locally so we aren't losing anything in doing that now.
To confirm our image sits in our local repo, run:
docker image ls
You should see the image in your local repo as:
Why do I have 3 dotnet images? Because I used them before. At the end of this posts you should have two of them. Guess which?
Creating our App
Let's now create our app. As always, we'll use the dotnet CLI, let's leave the Visual Studio tutorials to Microsoft, shall we? Open a terminal, navigate to your projects folder and create a root folder for our project. For example c:\src\webapp. Open a terminal, cd into that folder and type:
C:\src>dotnet new mvc -o webapp
The template "ASP.NET Core Web App (Model-View-Controller)" was created successfully.
This template contains technologies from parties other than Microsoft, see https://aka.ms/aspnetcore/3.1-third-party-notices for details.
Processing post-creation actions...
Running 'dotnet restore' on webapp.csproj...
Restore completed in 123.55 ms for C:\src\webapp\webapp.csproj.
Restore succeeded.
Now let's test our project to see if it runs okay by running:
The template "ASP.NET Core Web App (Model-View-Controller)" was created successfully.
This template contains technologies from parties other than Microsoft, see https://aka.ms/aspnetcore/3.1-third-party-notices for details.
Processing post-creation actions...
Running 'dotnet restore' on webapp.csproj...
Restore completed in 123.55 ms for C:\src\webapp\webapp.csproj.
Restore succeeded.
cd webapp
dotnet run
C:\src\webapp>dotnet run
info: Microsoft.Hosting.Lifetime[0]
Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\src\webapp
Open https://localhost:5001/, and confirm your webapp is similar to:dotnet run
C:\src\webapp>dotnet run
info: Microsoft.Hosting.Lifetime[0]
Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\src\webapp
Containerizing our web application
Let's now containerize our application. Learning this is a required step for those looking to get into microservices. Since containers are the new deployment unit it's also important to know that we can encapsulate our builds inside Docker images and wrap everything on a Docker file.Creating our first Dockerfile
A Dockerfile is the standard used by Docker (and OCI-containers) to perform tasks to build images. Think of it as a script containing a series of operations (and configurations) Docker will use. Since our super-simple web app does not require much, our Dockerfile can be as simple as:
# builds our image using dotnet's sdk
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /source
COPY . ./webapp/
WORKDIR /source/webapp
RUN dotnet restore
RUN dotnet publish -c release -o /app --no-restore
# runs it using aspnet runtime
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["dotnet", "webapp.dll"]
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /source
COPY . ./webapp/
WORKDIR /source/webapp
RUN dotnet restore
RUN dotnet publish -c release -o /app --no-restore
# runs it using aspnet runtime
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["dotnet", "webapp.dll"]
Why combine instructions?
Remember that Docker images are built using common layers and each command run will be ran on top of the previous one. The way we script our Dockerfiles affects how our images are built as each instruction will produce a new layer. In order to optimize our images, we should combine our instructions whenever possible.
Remember that Docker images are built using common layers and each command run will be ran on top of the previous one. The way we script our Dockerfiles affects how our images are built as each instruction will produce a new layer. In order to optimize our images, we should combine our instructions whenever possible.
Building our first image
Save the contents above as a file named Dockerfile on the root folder of your project (on the path above to where your csproj exists, on my case c:\src\webapp\Dockerfile) and run:
C:\src\webapp>docker build . -t webapp
Sending build context to Docker daemon 4.391MB
Step 1/10 : FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
---> fc3ec13a2fac
Step 2/10 : WORKDIR /source
---> Using cache
---> 18ca54a5c786
Step 3/10 : COPY . ./webapp/
---> 847771670d86
Step 4/10 : WORKDIR /source/webapp
---> Running in 2b0a1800223e
Removing intermediate container 2b0a1800223e
---> fb80acdfe165
Step 5/10 : RUN dotnet restore
---> Running in cc08422b2031
Restore completed in 145.41 ms for /source/webapp/webapp.csproj.
Removing intermediate container cc08422b2031
---> a9be4b61c2e6
Step 6/10 : RUN dotnet publish -c release -o /app --no-restore
---> Running in 8c2e6f280cb9
Microsoft (R) Build Engine version 16.5.0+d4cbfca49 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.
webapp -> /source/webapp/bin/release/netcoreapp3.1/webapp.dll
webapp -> /source/webapp/bin/release/netcoreapp3.1/webapp.Views.dll
webapp -> /app/
Removing intermediate container 8c2e6f280cb9
---> ceda76392fe7
Step 7/10 : FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
---> c819eb4381e7
Step 8/10 : WORKDIR /app
---> Using cache
---> 4f0b0bc1c33b
Step 9/10 : COPY --from=build /app ./
---> 26e01e88847d
Step 10/10 : ENTRYPOINT ["dotnet", "webapp.dll"]
---> Running in 785f438df24c
Removing intermediate container 785f438df24c
---> 5e374df44a83
Successfully built 5e374df44a83
Successfully tagged webapp:latest
If your build worked, run docker image ls and you should see your image webapp listed as an image by Docker:Sending build context to Docker daemon 4.391MB
Step 1/10 : FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
---> fc3ec13a2fac
Step 2/10 : WORKDIR /source
---> Using cache
---> 18ca54a5c786
Step 3/10 : COPY . ./webapp/
---> 847771670d86
Step 4/10 : WORKDIR /source/webapp
---> Running in 2b0a1800223e
Removing intermediate container 2b0a1800223e
---> fb80acdfe165
Step 5/10 : RUN dotnet restore
---> Running in cc08422b2031
Restore completed in 145.41 ms for /source/webapp/webapp.csproj.
Removing intermediate container cc08422b2031
---> a9be4b61c2e6
Step 6/10 : RUN dotnet publish -c release -o /app --no-restore
---> Running in 8c2e6f280cb9
Microsoft (R) Build Engine version 16.5.0+d4cbfca49 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.
webapp -> /source/webapp/bin/release/netcoreapp3.1/webapp.dll
webapp -> /source/webapp/bin/release/netcoreapp3.1/webapp.Views.dll
webapp -> /app/
Removing intermediate container 8c2e6f280cb9
---> ceda76392fe7
Step 7/10 : FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
---> c819eb4381e7
Step 8/10 : WORKDIR /app
---> Using cache
---> 4f0b0bc1c33b
Step 9/10 : COPY --from=build /app ./
---> 26e01e88847d
Step 10/10 : ENTRYPOINT ["dotnet", "webapp.dll"]
---> Running in 785f438df24c
Removing intermediate container 785f438df24c
---> 5e374df44a83
Successfully built 5e374df44a83
Successfully tagged webapp:latest
Remember the -t flag on the docker build command above? It told Docker to tag our own image as webapp. That way, we can use it intuitively instead of relying on its ID. We'll see next.
Running our image
Okay, now the grand moment! Run it with the command below in bold, we'll explain the details later:
C:\src\webapp>docker run --rm -it -p 8000:80 webapp
warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
Storing keys in a directory '/root/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
No XML encryptor configured. Key {a0030860-1697-4e01-9c32-8d553862041a} may be persisted to storage in unencrypted form.
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: /app
Point your browser to http://localhost:8000, you should be able to view our containerized website similar to:warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
Storing keys in a directory '/root/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
No XML encryptor configured. Key {a0030860-1697-4e01-9c32-8d553862041a} may be persisted to storage in unencrypted form.
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: /app
Reviewing what we did
Let's now recap and understand what we did so far. I intentionally left this to the end as most people would like to see the thing running and play a little with the commands before they get to the theory.Our website
Hope there isn't anything extraordinary there. We essentially scaffolded an ASP.NET Core MVC project using the CLI. The only point of confusion could be the location of the project and the location of the Docker file. Assuming all your projects sit on c:\src on your workstation, your should have:- c:\src\webapp: the root for our project and our ASP.NET Core MVC website
- c:\src\webapp\Dockerfile: the location for our Dockerfile
Dockerfile
When we built our Dockerfile you may have realized that we utilized two images (the SDK and the ASP.NET image). Despite this being a little less intuitive for beginners, it's actually a good practice as our images will be smaller in size and have less deployed code making them more secure as we're reducing the attack surface. The commands we used today were:- FROM <src>: tells Docker that to pull the base image needed by our image. The SDK image contains the tools necessary to build our project and the ASP.NET image to run it.
- COPY .: copies the contents of the current directory to the specified inside the container.
- RUN <cmd>: runs a command inside the container.
- WORKDIR <path>: set the working directory for subsequent instructions and also as startup location for the container itself.
- ENTRYPOINT [arg1, arg2, argN]: specifies the command to execute when the container starts up in an array format. In our case, we're running dotnet webapp.dl on the container as we would do to run our published website outside of it.
Docker Build
Next let's review what the command docker build . -t webapp means:- docker build .: tells Docker to to build our image based on the contents of the current folder (.). This command also accepts an optional Dockerfile which we didn't provide on this case. When not provided, Docker expects a Dockerfile on the current folder which you copied and pasted before running this command.
- -t webapp: tag the image as webapp so we can run commands using this friendly name
Docker Run
To finish, let's understand what docker run --rm -it -p 8000:80 webapp means:- docker run: runs an instance of an image (a container);
- --rm: remove the image just after it finishes. We did this to not pollute your local environment as you'll probably run this command multiple times and each run will produce a new container. Note that you should only use this in development as Docker won't preserve the logs for the image after it's deleted;
- --it: keep it running attached to the terminal so we can see the logs and cancel it with Ctrl-C;
- -p 8000:80: expose the container's port 80 on the localhost at port 8000. Is that port used? Feel free to change the number before the : to something that makes sense to you, just remember to point your browser correctly to the new port.
- webapp: the tag of our image
Troubleshooting
So, it may be possible that you couldn't complete your tutorial. Here's some tips that may help.Check your Dockerfile
The syntax for the Dockerfile is very specific. Make sure you copied the file correctly and your names/folders match mine. Also make sure you saved your Dockerfile on the root of your project (or, in the same folder as your csproj) and that you ran docker build from the same folder.Run interactively
In the beginning, always run the container interactively by using the -it syntax as below. After you get comfortable with the Docker CLI, you'll probably want to run them in detached mode (-d).
docker run --rm -it --name w1 webapp
List your images
Did you make sure your image was correctly built? Do you see webapp when you run:
docker image ls
You can also list your images with docker images however, I prefer the above format as all other commands follow that pattern, regardless of the resource you're managing.
List the containers
If you ran your container, Did you make sure your image was correctly built? Do you see webapp when you run:
docker container ls
Use HTTP and not HTTPS
Make sure that you point your browser to http://localhost:8000 (and not https). Some browsers are picky today with HTTP but it's what works on this example.Check for the correct port
Are you pointing to the correct url. The -p 8000:80 param specified previously tells Docker to expose the container's port 80 on our host at 8000.Search your container
It's possible that your container failed. To list all containers that ran previously, type:
docker container ls -a
Inspect container information
To inspect the metadata for your container type the command below adding your container id/name. This command is worth exploring as it will teach you a lot about the internals of the image.
docker container inspect <containerid>
Removing containers
If you want to get rid of the containers, run:
docker container prune -f
Removing Images
If you want to get rid of the containers, run:
docker image rm <imageid>
Check the logs
If you managed to create the image and run it, you could check the logs with:
docker container logs <container-id>
Log into your container
You could even log into your container and if you know some Linux, validate if the image contains what you expect. The command to connect to a running container is:
docker exec -it <containerid> bash
Install tools on your container
You could even install some tools on your container. For Debian (the default image), the most essential tools (and their packages) I needed were:- ps: apt install procps
- netstat: apt install net-tools
- ping: apt install iputils-ping
- ip: apt install iproute2
Don't forget to run apt update to update your local cache of files else the commands above won't work.
Conclusion
On this post we reviewed how to create an ASP.NET Core website with Docker. Docker is a very mature technology and essential for those looking into transitioning their platforms into microservices.Source Code
As always, the source code for this article is available on GitHub.References
- Donet Core Official Image | DockerHub
- Dockerize an ASP.NET Application | Docker
- Host ASP.NET Core in Docker containers | Microsoft
- Donet run | Microsoft
- Dockerfile reference | Docker
- Host ASP.NET Core in Docker containers | Microsoft
- Dotnet Docker | GitHub
See Also
- Microservices in ASP.NET
- My journey to 1 million articles read
- Adding Application Insights to a ASP.NET Core website
- Building and Hosting Docker images on GitHub with GitHub Actions
- Deploying Docker images to Azure App Services
- Hosting Docker images on GitHub
- How to push Docker images to ACR - Azure Container Registry
- Send emails from ASP.NET Core websites using SendGrid and Azure
- Async Request/Response with MassTransit, RabbitMQ, Docker and .NET core
- Distributed caching in ASP.NET Core using Redis, MongoDB and Docker
- How to build and run ASP.NET Core apps on Linux
- How to create a Ubuntu Desktop on Azure
- How I fell in love with i3