Monday, August 10, 2020

Creating ASP.NET Core websites with Docker

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

Docker is one the most used and loved technology on the market today. We already discussed its benefits, how to install it and even listed technical details every developer should know. On this post, we will review how to create an ASP.NET Core website with Docker Desktop using the latest .NET Core 3.1. After reading this post you should understand how to:
  • 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:
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
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:
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:

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"]
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. 

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:
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:

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

    See Also

    About the Author

    Bruno Hildenbrand