Containerizing F# with Docker

Read Time: 9 minutes

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
2
dotnet new console -lang F# -n SampleConsole
cd SampleConsole
1
2
3
4
5
6
open System

[<EntryPoint>]
let main argv =
printfn "Hello from F# in Docker."
0

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.

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM 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"]

.dockerignore

1
2
3
bin
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
2
docker build -t sample-console .
docker run sample-console

Sample Console Results

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
2
dotnet new giraffe -lang F# -n SampleWeb 
cd 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  <PropertyGroup>
<!-- Change from netcoreapp version from 2.1 to 3.1 -->
<TargetFramework>netcoreapp3.1</TargetFramework>
...
</PropertyGroup>
...
<ItemGroup>
<!-- Remove PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.*" -->
...
</ItemGroup>
...
<ItemGroup>
<!-- Add certificate configuration -->
<None Include="certificate.json" CopyToOutputDirectory="PreserveNewest" />
...
</ItemGroup>
</Project>

Here are the web.config modifications.

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<handlers>
<!-- Change AspNetCoreModule to AspNetCoreModuleV2 -->
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet" arguments="SampleWeb.dll" stdoutLogEnabled="false" stdoutLogFile="logs/stdout" />
</system.webServer>
</configuration>

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
open System.Security.Cryptography.X509Certificates
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.Hosting
open Microsoft.AspNetCore.Server.Kestrel.Core
...
let configureApp (app: IApplicationBuilder) =
// Former: let env = app.ApplicationServices.GetService<IHostingEnvironment>()
let env = app.ApplicationServices.GetService<IWebHostEnvironment>()
...
let configureAppConfiguration (config: IConfigurationBuilder) =
config.AddJsonFile("/https/certificate.json") |> ignore

let configureKestrel (context: WebHostBuilderContext) (options: KestrelServerOptions) =
let certificatePath = context.Configuration.GetSection("certificate").GetValue<string>("Path")
let certificatePassword = context.Configuration.GetSection("certificate").GetValue<string>("Password")
let certificate = new X509Certificate2(certificatePath, certificatePassword)
options.ListenAnyIP(5000)
options.ListenAnyIP(5001, fun options -> options.UseHttps certificate |> ignore)
...
WebHostBuilder()
.ConfigureAppConfiguration(configureAppConfiguration)
.UseKestrel(Action<WebHostBuilderContext,KestrelServerOptions> configureKestrel)
...
.Build()
.Run()

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
2
3
4
5
6
{
"Certificate": {
"Path": "/https/SampleWeb.pfx",
"Password": "password"
}
}

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.

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM 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
2
3
bin
obj
Dockerfile

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
2
docker build -t sample-web .
docker run -p 5000:5000 -p 5001:5001 -v ${HOME}/.aspnet/https:/https/ sample-web

Sample Web Results

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.