Containerizing an F# application using Docker is a fairly straight forward process. Today we’ll take a look at doing just that.
This post will cover putting different types of F# applications in Docker containers. If you want to follow along, you’ll need to get a couple things. You’ll need to install Docker and .NET Core. As of right now, dotnet core is at 3.1, so that will be the target version.
The process follows a common pattern, although there is some divergence based on the specific use case. This can eaily be shown using two examples. The first example is a console app in Docker. It won’t do much, since I don’t want to distract from the core docker implementation. The second example is putting a web app in Docker. This example is more involved for a couple reasons, as you’ll see below.
Dockerizing a console application
First, create the application. This is a standard process for any F# application. As promised, it will be about as boring an application as possible.
1 | dotnet new console -lang F# -n SampleConsole |
1 | open System |
Once the application is in place, it’s time to add the docker components. In the SampleConsole directory I’ll add a Dockerfile
for the build and .dockerignore
to excluded files. The .dockerignore
isn’t strictly necessary, but it does keep the container cleaner. Using a multi-stage Dockerfile makes the implementation pretty easy. Since Microsoft provides dotnet core images, I leverage those. The sdk one for building, and the runtime one for final execution. To containerize the application, copy the application source into /src. Then build from /src into /app. Then run out of /app. The alpine flavor is used to keep the image smaller. There is a non-alpine version (just remove the “-alpine” from the image names) if that is so desired. In some ways this process is anti-climatic, there isn’t much here, but that is kind of the point. Sometimes it is nice when things just work.
Dockerfile1
2
3
4
5
6
7
8
9
10
11
12
13
14
15FROM mcr.microsoft.com/dotnet/core/sdk:3.1-alpine AS build
# build application
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c release -o /app --no-self-contained --no-restore
# final stage/image
FROM mcr.microsoft.com/dotnet/core/runtime:3.1-alpine
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["./SampleConsole"]
.dockerignore1
2
3bin
obj
Dockerfile
At this point, the only thing left to do is build and run the application. The output is relatively uninspiring, but at least it is a good first step to doing something slightly more interesting.
1 | docker build -t sample-console . |
Dockerizing a web application
Entering the second part of the post, putting an F# web application in a container. Much of the above still applies, but considering the way of web apps, it needs to be different. The first is initializing the app. There are several good dotnet core templates to choose from, but I’m going to specifically use Giraffe.
1 | dotnet new giraffe -lang F# -n SampleWeb |
This is where a little surgery takes place. Like the above example, I want to use dotnet core 3.1, but the Giraffe template is for v2.1. I’ll need to tweak a couple parts to get it to work together. I’ll break them down by file. Here are the SampleWeb.fsproj modifications. Half of this is obvious, the slightly less obvious is the certificate.json reference, this will be used for Kestrel configuration later.
1 | <PropertyGroup> |
Here are the web.config modifications.
1 | <?xml version="1.0" encoding="utf-8"?> |
Here are the Program.fs modifications. For the most part the web app stays intact, but there are two specific parts to call out. IHostingEnvironment
works, but is deprecated. As a result I’ll refactor to use IWebHostEnvironment
. It removes a warning and future-proofs things a bit. The big thing is Kestrel configuration. The slightly short version is that the Kestrel changes are not required if running the application outside of Docker. But, since the target is Docker, I ran into some complications. I need to explicitly configure Kestrel so it runs https properly while in the container. To do this I add a configuration step, using a certificate.json. I then use that to create a certificate object that can be used in Kestrel configuration. I’ll run http over 5000 and https over 5001; this will be useful information in a later step.
1 | open System.Security.Cryptography.X509Certificates |
Now to add the supporting certificate.json file. Here I show a good and a bad practice. First the good, by creating the configuration file and certificate outside of the project, I won’t include secrets in the image itself. Second the bad, “password” is not a good certificate password.
1 | { |
One last piece is needed for ssl support. Normally production would have a proper cert, but since this just dev I’ll export my dotnet dev cert and use that for my Docker instance. Below is how to export the cert.
1 | dotnet dev-certs https -ep ~/.aspnet/https/SampleWeb.pfx -p password |
Now that the necessary changes are in place, it’s time to do the containerization part. In the SampleWeb directory I’ll add Dockerfile
and .dockerignore
files. As before, the Dockerfile
implements the application build as well as final packaging of the application using alpine. You may note that the final stage here uses aspnet-3.1-alpine
, where the previous version was runtime-3.1-alpine
.
Dockerfile1
2
3
4
5
6
7
8
9
10
11
12
13
14FROM mcr.microsoft.com/dotnet/core/sdk:3.1-alpine AS build
WORKDIR /src
COPY src/SampleWeb/. .
RUN dotnet restore
RUN dotnet publish -c release -o /app --no-self-contained --no-restore
# create final image
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-alpine
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["./SampleWeb"]
.dockerignore
1 | bin |
At this point, the only thing left to do is build and run the application. Since this is a web server, I’ll need to map the necessary ports. I also map the directory containing certificate.json and SampleWeb.pfx into the container. I will note a specific omission for a real-world implementation. Kestrel can now be used as an edge server, although my preference is to put it behind a reverse proxy like nginx. That topic is beyond the scope of this post. At least in the current configuration http and https can be tested locally.
1 | docker build -t sample-web . |
This has been a brief look into how Docker can be leveraged with F#. This is only the begining of what can be done, so hopefully you can use this as a jumping point for your projects. Until next time.