Sample App Dependencies: ASP.Net Core and Docker Packages
To build an app in Docker, first we need an app to Dockerize. This tutorial uses the ASP.Net Core project from a previous blog post on Adding Login to Your ASP.NET Core MVC App. That blog post shows you how to build a simple .Net Core application that uses Okta for Identity Management. You should work through that blog post, or at very least read it and clone the repo.
You’ll also need:
Build the ASP.NET Core Application
Docker allows you to build an application in pretty much the same way you would create an application to run on your local machine. To get started quickly, clone this git repo:
git clone https://github.com/oktadeveloper/okta-aspnet-mvc-core-sqlite-example.git
Configure Identity Management for Your ASP.NET Core App
First things first, set up an application that will provide us with identity management using the Okta developer console:
- Log into The Okta Developer Console
- Select Applications from the top menu
- Click the Add Application button
- Select Web as the platform and click Next
- On the Settings page:
- Name: MyOktaApp
- Base URIs:
https://localhost:5001
- Login Redirect URIs:
https://localhost:5001/authorization-code/callback
- Click Done to create the application
Once you’ve created the app, click Edit to change a few settings:
- Logout redirect URIs:
https://localhost:5001/signout-callback-oidc
- Initiate login URI:
https://localhost:5001/authorization-code/callback
At the bottom of the page, you’ll see Client Credentials, including a ClientID and a Client secret. Take note of these for later use.
Update the Settings in the ASP.NET Core App
The sample application you created has identity management configuration stored in appsettings.json
, unless you cloned the repo above rather than working through the complete post. In real life, you shouldn’t store this configuration in your source code for security reasons. In this post, we will demonstrate how to reliably pass dynamic configuration to your application, and close this security gap. Start by removing the settings.
Edit appsettings.json
to remove:
"Okta": {
"ClientId": "{OktaClientId}",
"ClientSecret": "{OktaClientSecret}",
"Domain": "https://{yourOktaDomain}.okta.com"
},
To test this change:
dotnet run
Should return:
An exception of type 'System.ArgumentNullException' occurred in Okta.AspNet.Abstractions.dll but was not handled in user code: 'Your Okta URL is missing. Okta URLs should look like: https://{yourOktaDomain}. You can copy your domain from the Okta Developer Console.'
Nice work!
Use Docker to Containerize the ASP.NET Core App
Docker is a collection of virtualization technologies wrapped up in an easy to use package. Don’t let “virtualization” trip you up, though. Docker doesn’t deal with virtual machines; instead, it works by sharing a kernel between multiple isolated containers. Each one of these containers operates utterly unaware of other containers that may be sharing the same kernel. Virtual machines, in contrast, run multiple discrete operating systems on top of a virtualized hardware platform that, itself, runs atop the host operating system. Docker is much more lightweight, and many Docker containers can run on a single host machine.
Build the ASP.NET Core App Using Docker
Let’s put Docker to work. The key to Dockerizing an application is the Dockerfile
. Add one to the root of your project with the following contents to get started:
FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS build
WORKDIR /src
COPY ["OktaMvcLogin.csproj", "./"]
RUN dotnet restore "./OktaMvcLogin.csproj"
COPY . .
RUN dotnet build "OktaMvcLogin.csproj" -c Release -o /app
The uppercase words are Docker commands. There aren’t many of them, and you can find the details of them all at Docker’s website.
FROM
tells Docker which image you want to use for your container. An image is a compressed file system snapshot. Also, the result of building a Dockerfile
is a new image. So, one way to look at a Dockerfile
is as a series of transformations that convert one image into another image that includes your application.
WORKDIR
tells Docker which directory to use for performing subsequent commands.COPY
tells Docker to copy a file from your local filesystem into the container image.RUN
executes commands within the container image.
So, in plain English - this Dockerfile
is based on the dotnet/core/sdk
image hosted at mcr.microsoft.com
. Docker copies the .csproj
file from your local working directory to create your image and dotnet restore
restores all the referenced packages. Once that’s done, Docker copies the remaining files from your working directory, then dotnet build
creates a Release build at /app
.
Manage Dependencies Efficiently with Docker
Reading this, you may be thinking, why bother to copy the project file and run restore
before copying the source code and running build
? Why not copy everything then build and restore in one step? The answer is caching. Every time a Dockerfile
modifies the docker image, Docker creates a snapshot. If you copy a file or run a command to install a package, Docker captures the differences in a new snapshot. Docker then caches and reuses the snapshots if the image hasn’t changed. So, by restoring dependencies as a separate step, the image snapshot can be reused for every build, as long as the dependencies haven’t changed. This process speeds up the build considerably since downloading dependencies can take some time.
Run the ASP.NET Core App in a Docker Container
As mentioned above, a Dockerfile
can be considered a series of filesystem transformations. Your current file transforms the Microsoft-provided SDK container into a new container with both the Microsoft SDK and a release build of your application stored at /app
.
Try this out::
# Build an image using the Dockerfile in the current directory
docker build --target build -t oktamvclogin .
# Run the image, executing the command 'ls /app'
docker run -it oktamvclogin ls /app
You’ll see that the app
folder in your container image contains the Release build output for your project.
Remove ASP.NET Core Development Tools from Your Docker Image
So far, you’ve built your application within a Docker container. Nice work!
However, remember Docker, as a tool, reduces the number of moving parts in your application. To improve reliability by eliminating unnecessary dependencies, we also need to remove development tools, which can cause conflicts and open security risks. The Microsoft-provided SDK image includes development tools, so let’s look at how to get rid of them.
Add the following lines to your Dockerfile
:
FROM build AS publish
RUN dotnet publish "OktaMvcLogin.csproj" -c Release -o /app
FROM mcr.microsoft.com/dotnet/core/aspnet:2.2 AS base
WORKDIR /app
EXPOSE 5001
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "OktaMvcLogin.dll"]
You will see a few FROM
commands, each with an AS
clause. This syntax provides multi-stage builds, the key to getting rid of unnecessary dependencies. In plain English, your build process is now:
- Use the SDK image to create a release build of the application. Call this stage ‘build’
- Use the ‘build’ stage image to publish the application to the ‘app’ folder. Call this stage ‘publish’
- Download the Microsoft-provided ASP.NET core image, which has only runtime components. Call this stage ‘base’
- Using the ‘base’ stage image, copy the contents of the ‘app’ directory from the ‘publish’ stage. Call this stage ‘final’
So, your Dockerfile
now uses the SDK image to build your application, then discards that image and uses a runtime image to run the application.
Run the ASP.NET Core Application in Docker
The ENTRYPOINT
command merits special attention. So far you’ve seen how Dockerfiles define a series of filesystem transformations, but more often than not, a Docker container is executable. By that, I mean that you run the container in Docker, and the result is a fully configured, running application. ENTRYPOINT
is one of the mechanisms that make that work. When you run a container, Docker executes the command specified by the ENTRYPOINT
. In the case of your application, that command is dotnet OktaMVCLogin.dll
.
So now…
docker build -t oktamvclogin .
docker run oktamvclogin
… throws the same exception as before:
Unhandled Exception: System.ArgumentNullException: Your Okta URL is missing. Okta URLs should look like: https://{yourOktaDomain}. You can copy your domain from the Okta Developer Console.
Only this time, it’s Dockerized. How’s that for progress?
The application doesn’t work because you removed the sensitive configuration from appsettings.json
.
Pass Configuration to Docker
To fix this problem, we need to pass configuration to the Docker container as environment variables. ASP.NET Core picks up all environment variables prefixed with ASPNETCORE_
and converts __
into :
. To pass the configuration values for Okta:ClientId
, Okta:ClientSecret
and Okta:Domain
modify your command like this:
docker run -e ASPNETCORE_Okta__ClientId="{yourClientId}" \
-e ASPNETCORE_Okta__ClientSecret="{yourClientSecret}" \
-e ASPNETCORE_Okta__Domain="https://{yourOktaDomain}" \
oktamvclogin
This time the result will be a bit healthier:
Hosting environment: Production
Content root path: /app
Now listening on: 'http://[::]:80'
Application started. Press Ctrl+C to shut down.
NOTE: you may also see a ‘No XML Encryptor’ warning. You can ignore that for this walkthrough.
Configure Docker Networking
From this message, you might think you could go to http://localhost
and see your app in all its glory. However, your app runs in a container and listens on port 80 but, by default, your local machine cannot access port 80 on your container. Remember, your container runs in its own little world. That world has its own virtual network, and it’s locked down by default.
Thankfully, you can quickly get access to your container’s virtual network by mapping a port on your local machine to a port on your container.
docker run -e ASPNETCORE_Okta__ClientId="{yourClientId}-" \
-e ASPNETCORE_Okta__ClientSecret="{yourClientSecret}" \
-e ASPNETCORE_Okta__Domain="{yourOktaDomain}" \
-p 5001:80 \
oktamvclogin
Now, if you open a browser and go to http://localhost:5001
(because you mapped port 5001 to port 80 in your container), Et voila!
NOTE: this approach is suitable for development. However, for production workloads, Docker offers a comprehensive set of options designed for managing virtual networks. For more information see the networking overview in Docker’s documentation.
Configure SSL/TLS for Your Docker Image
If you click on the Login link in your application, chances are you’ll get an error from Okta with a message:
Description: The 'redirect_uri' parameter must be an absolute URI that is whitelisted in the client app settings.
This problem happens because when you configured the application in the Okta dashboard, you specified that the redirect URL was HTTPS. Now, since you accessed the site using HTTP, the redirect URL doesn’t match, and you get this error. One solution is to update the redirect URL in the Okta application. While that will work, it’s a bad idea. The redirect contains sensitive information and to prevent it from being read while in transit, it should be protected using a TLS channel.
Create a self-signed certificate
To set up TLS, you’ll need a certificate. In real life, you’d buy one from a reputable provider, but for this walkthrough, a self-signed certificate will do the job.
Generate a certificate:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365
To use the certificate with Kestrel (the ASP.NET Core webserver), we need to create a pfx
file that has both the public and private keys. You can do that using:
openssl pkcs12 -export -out webserver.pfx -inkey key.pem -in cert.pem
As part of the certificate creation process, you’ll be prompted to create an Export password. Be sure to take note of this as you’ll need it later to use the certificate. You’ll also be walked through the configuration process (Country Name, State or Province, etc.).
Add the certificate to the Docker image
You’ve created a certificate in your local filesystem. To use it in your Docker container, modify the Dockerfile to copy it into the final image.
Change the `final’ stage to:
FROM base AS final
ENV ASPNETCORE_URLS="https://+"
ENV ASPNETCORE_Kestrel__Certificates__Default__Path="./webserver.pfx"
WORKDIR /app
COPY --from=publish /app .
COPY webserver.pfx .
ENTRYPOINT ["dotnet", "OktaMvcLogin.dll"]
Setting the ASPNETCORE_URLS environment variable to https://+
ensures that the webserver only listens for https requests.
Since you’ve changed the Dockerfile, you can rebuild the image and run it:
docker build -t oktamvclogin .
docker run -e ASPNETCORE_Okta__ClientId="{yourClientId}" \
-e ASPNETCORE_Okta__ClientSecret="{yourClientSecret}" \
-e ASPNETCORE_Okta__Domain="{yourOktaDomain}" \
-e ASPNETCORE_Kestrel__Certificates__Default__Password="{yourExportPassword}" \
-p 5001:443 \
oktamvclogin
Notice the additional environment variable with the certificate export password and also, that the port mapping has changed from port 80 to port 443.
You can now navigate to https://localhost:5001
, and this time, you’ll be able to log in and use the sample application correctly.
NOTE: Since you’re using a self-signed certificate, your browser may display a warning page. You can safely ignore this warning.
Not the Best Way to Start Your Docker Container
Converting an application to run in Docker is relatively straightforward and offers significant benefits. However, passing a whole load of configuration to docker run
isn’t particularly user-friendly and is error-prone. Thankfully, the good folks at Docker have already come up with a solution to this problem - docker-compose
.
Using Docker Compose is pretty straightforward. Create a file named docker-compose.yml
in the same folder as your Dockerfile
and source code and add the following:
version: "3"
services:
web:
build: .
image: oktamvclogin
ports:
- "5001:443"
environment:
- ASPNETCORE_Okta__ClientId: "{yourClientId}"
- ASPNETCORE_Okta__ClientSecret: "{yourClientSecret}"
- ASPNETCORE_Okta__Domain: "{yourOktaDomain}"
- ASPNETCORE_Kestrel__Certificates__Default__Password: "{yourExportPassword}"
The docker-compose
file contains all values you previously passed to the docker run
command.
Now, to run the application:
docker-compose up
And it all just works. Happy days.
Of course, this is barely scratching the surface of what you can do with Docker Compose. To find out more, check out the official overview of Docker Compose
Containerize and configure an ASP.NET Core app with Docker
By working through this post, you’ve learned how to:
- Containerize an ASP.NET Core application with Docker
- Pass configuration to a Docker container
- Configure SSL/TLS for a containerized ASP.NET Core application
- Use
docker-compose
to efficiently run a Docker container with a particular configuration
Now, by including a Dockerfile
along with your source code, any developer can build your app – reliably. They can build it in any environment, as long as they have Docker installed, and it will work in precisely the same way. Every time, without exception. No dependency issues, no operating system hassles. It will just work. Not only that, but you can deploy the same container directly to production without any further modification.