Shipping F# to Production
Prerequisites: Testing with Expecto and xUnit
Publishing
dotnet publish produces everything needed to run your app:
dotnet publish -c Release -o out
dotnet publish -c Release -r linux-x64 --self-contained -o out-linux
dotnet publish -c Release -r win-x64 -p:PublishSingleFile=true
- Framework-dependent (the default): small output; the server needs the .NET runtime installed.
- Self-contained (
--self-contained): includes the runtime, so it runs on a machine with no .NET installed. - Single file: one executable to copy around.
- ReadyToRun (
-p:PublishReadyToRun=true) precompiles code for faster startup. Native AOT is available for .NET, but F# support is limited: reflection-heavy features such as%Aformatting and some serializers don't work with it.
Docker
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish src/Shop.Api -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "Shop.Api.dll"]
A multi-stage build keeps the SDK out of the final image. The same image runs on any container host: Azure App Service, AWS, Kubernetes, or a plain Linux server.
Code quality
- Fantomas — format everything the same way; check it in CI with
dotnet fantomas --check .. - FSharpLint and Ionide F# analyzers — catch style issues and common mistakes.
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>in the project — especially for incomplete pattern matches (FS0025).<WarnOn>3390</WarnOn>checks XML doc comments;<Nullable>enable</Nullable>turns on nullness checking (F# 9+).
Configuration, logging, and errors
- Read settings from environment variables and
appsettings.jsonthrough ASP.NET Core's configuration system — never hard-code secrets. - Log with
Microsoft.Extensions.Logging(or Serilog) using structured messages:logger.LogInformation("Order {OrderId} placed", id). - Convert
Resulterrors to proper HTTP status codes at the edge, and add a global exception handler for the truly unexpected.
Security
- Validate all external input by parsing it into domain types (smart constructors) — never trust JSON, query strings, or files.
- Use parameterized queries (Dapper, Npgsql, or SqlClient with parameters) — never build SQL with string concatenation.
- Keep packages updated:
dotnet list package --vulnerablereports known vulnerabilities. - Be careful with type providers and scripts that fetch code or schemas from the network at build time.
Continuous integration
A typical pipeline, on GitHub Actions or any CI system:
dotnet tool restore
dotnet fantomas --check .
dotnet build -c Release
dotnet test -c Release --no-build
dotnet publish src/Shop.Api -c Release -o out
Where F# runs in production
- Finance — trading, risk, and pricing systems at banks and hedge funds.
- Energy and insurance — modeling and calculation engines.
- Web services — APIs and back ends built with Giraffe or ASP.NET Core.
- Developer tools — the F# compiler itself, Fantomas, and many analyzers are written in F#.
What's next
Try the Advanced Projects, then read the final lesson for a summary and ideas for where to go from here.