ASP.NET image based on Alpine Linux, supporting .NET 3.1, 5.0 and 6.0 (preview).
100K+
Docker Hub - Quay.io - bfren ecosystem
An image for running a published ASP.NET Core application, supervised by S6, with a simple way to deploy new versions without rebuilding the image.
It is Alpine + S6 with:
aspnet service that runs your application as the non-root www userlive and publish - so you can upload a new version and switch to it, and switch back if it goes wrongIt only contains the runtimes, not the SDK - build and publish your application elsewhere (dotnet publish), then run the output here. There is no SSL support - it is designed to run behind a reverse proxy such as Caddy.
Publish your application, then run it from ./publish:
dotnet publish -c Release -o ./publish
docker run -d --name app -p 5000:5000 \
-e BF_ASPNET_ASSEMBLY=MyApp.dll \
-v ./publish:/app/live \
quay.io/bfren/aspnet:net10
Or with Docker Compose:
services:
app:
image: quay.io/bfren/aspnet:net10-6.1
environment:
- BF_ASPNET_ASSEMBLY=MyApp.dll
ports:
- "5000:5000"
volumes:
- ./v/live:/app/live
- ./v/publish:/app/publish
Open http://localhost:5000. BF_ASPNET_ASSEMBLY is the file name of your application's main .dll, inside /app/live.
/app/publish, then aspnet-switch - and switch again to roll backwww user, with its files read-onlyen-US and en-GB - see Globalisation for other culturesTimeZoneInfo can convert between time zonesImages are published to Docker Hub (bfren/aspnet) and Quay.io (quay.io/bfren/aspnet) for linux/amd64, linux/arm/v7 and linux/arm64. Tags start with the .NET version.
| Tag | Example | Use when |
|---|---|---|
netX | net10 | You want the latest image for a .NET version |
netX-MAJOR | net10-6 | You want updates, but not breaking changes |
netX-MINOR | net10-6.1 | You want fixes only - recommended for building on |
netX-VERSION | net10-6.1.21 | You want to pin this image exactly |
netX.Y.Z | net10.0.12 | You want an exact .NET runtime release |
There is no latest tag - always choose a .NET version.
| .NET | Runtime | Type | Alpine | Microsoft support ends |
|---|---|---|---|---|
| 11 | 11.0.0-rc.1 | Release candidate | 3.24 | n/a - not yet released |
| 10 | 10.0.12 | Long-term support | 3.24 | 2028-11-14 |
| 9 | 9.0.20 | Standard support | 3.24 | 2026-11-10 |
| 8 | 8.0.31 | Long-term support | 3.24 | 2026-11-10 |
.NET 8 and 9 both reach the end of support in November 2026 - use .NET 10 for anything new, and don't use .NET 11 in production until it is released. Source: .NET Support Policy
| Port | Purpose |
|---|---|
5000 | HTTP - your application (set by ASPNETCORE_URLS). Use a proxy for SSL. |
| Volume | Purpose |
|---|---|
/app/live | The application that is running. |
/app/publish | Upload a new version here, then run aspnet-switch. Holds the previous version after. |
| Variable | Values | Description | Default |
|---|---|---|---|
BF_ASPNET_ASSEMBLY | file name | Required. Your application's main .dll, relative to /app/live. | blank |
ASPNETCORE_URLS | URL | Where the application listens. The health check uses it too - keep it to one URL. | http://+:5000 |
DOTNET_CONTENTROOT | path | The application's content root. | /app/live |
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT | true, false | Set to true to turn off globalisation support. | false |
DOTNET_RUNNING_IN_CONTAINER | true | Tells .NET it is running in a container. | true |
BF_DEBUG | 0, 1 | Show detailed output from bf scripts. | 0 |
Alpine + S6 adds more variables (timezone, cron).
When the container starts, Alpine + S6 runs the initialisation scripts. This image adds two: 10-env sets the application paths, and 11-version checks the ASP.NET runtime and shows its version. Then S6 starts the aspnet service, which runs dotnet /app/live/$BF_ASPNET_ASSEMBLY as the www user.
If the assembly doesn't exist yet, the service says so, waits 30 seconds and exits - S6 then starts it again. So you can start the container first and publish your application afterwards. Unlike most images in the ecosystem, the container does not stop when the application fails - S6 restarts it.
Globalisation is on (DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false), so .NET uses ICU for cultures, sorting and formatting. Alpine's icu-libs comes with icu-data-en, which only contains the en-US and en-GB locales. Other cultures (e.g. fr-FR) don't cause an error - formatting and sorting quietly fall back to the default data instead.
If your application needs other cultures, add Alpine's full ICU data in your own image - it adds about 32 MB:
RUN apk add --no-cache icu-data-full
tzdata is installed, so TimeZoneInfo.FindSystemTimeZoneById("Europe/London") and other IANA time zones work. The container's own time zone is set by BF_TZ (default Europe/London).
At start-up, and after every switch, ownership and permissions are applied:
| Folder | Owner | Files | Folders |
|---|---|---|---|
/app/live | www:www | 0400 | 0500 |
/app/publish | www:www | 0600 | 0700 |
Your application can read its own files but not change them. Store anything it writes (uploads, logs, data protection keys) in another volume.
dotnet publish into /app/publish (on the host, the folder mounted there).docker exec <container> aspnet-switch.aspnet-switch swaps the two folders - the new version moves to /app/live, the old one to /app/publish - re-applies the permissions, and restarts the application. To roll back, run aspnet-switch again.
aspnet-switch-terminate does the same swap, then stops the container instead of restarting the application, so your restart policy (e.g. restart: unless-stopped) starts it again cleanly.
aspnet-restart stops the aspnet service, stops any remaining dotnet processes, then starts the service again. It runs automatically every day at 02:00, and you can run it yourself:
docker exec <container> aspnet-restart
The health check (/etc/bf/health.d/10-aspnet.nu) loads ASPNETCORE_URLS (with + replaced by localhost, so http://localhost:5000 by default) and passes if the HTTP status is between 200 and 399. Docker runs it every 30 seconds (every 2 seconds during the first 60 seconds). If your application doesn't answer at / - an API, for example - replace /etc/bf/health.d/10-aspnet.nu in your own image.
bf-aspnet module| Command | Use |
|---|---|
bf-aspnet app disable | Stop the aspnet service and any dotnet processes. |
bf-aspnet app restart | Stop, then start the application again (used by aspnet-restart). |
bf-aspnet app switch | Swap /app/publish and /app/live. Add --terminate to stop the container afterwards. |
The helpers on the PATH are aspnet-restart, aspnet-switch and aspnet-switch-terminate.
To ship your application inside the image rather than mounting it:
FROM quay.io/bfren/aspnet:net10-6.1
ENV BF_ASPNET_ASSEMBLY=MyApp.dll
COPY ./publish /app/live
Add an overlay/tmp/install and RUN bf-install for anything else your application needs - see Alpine and Alpine + S6.
./test.nu # builds the .NET 10 image and runs /test
./test.nu 8 # the same for .NET 8
./test.nu --all # every version, in parallel
./run.sh # builds the .NET 10 image and opens a shell in it
/test runs this image's tests alongside the base images' tests, and fails if tests don't execute:
dotnet runs, the .NET and ASP.NET Core runtimes match the version in the build log, ICU is installed, and the 11-version start-up script finds the runtime.aspnet-switch swaps the folders, rolls back when run twice, refuses when nothing is published, and applies the permissions. These use temporary folders, never /app.Run it for each .NET version you change, as each has its own runtime and dependencies. run.sh mounts an application folder from the author's machine - change the path to one of yours.
BF_ASPNET_ASSEMBLY must be the file name of your main .dll (e.g. MyApp.dll, not a path), and the file must be in /app/live. Check the volume is mounted to /app/live and not /app.
The health check loads /. If your application returns 404 or 401 there, replace /etc/bf/health.d/10-aspnet.nu in your own image (see Health check).
/app/live is read-only to the application. Mount another volume for anything it writes, and point your application at it.
aspnet-switch says "Please publish your application first"/app/publish is empty - copy the new version there first.
live and publish instead of rebuilding the image?Uploading files and switching is quicker than building and pushing an image for every change, it keeps the previous version ready, and rolling back is one command.
So that you can start it before the application is published, and so a crash only needs S6 to restart the application. Use aspnet-switch-terminate if you want a deployment to restart the whole container.
It is a release candidate - fine for testing, not for production. It will be updated when .NET 11 is released.
Copyright (c) 2021-2026 bfren (unless otherwise stated)
Content type
Image
Digest
sha256:cb0fdbdd9…
Size
80.3 MB
Last updated
about 3 hours ago
docker pull bfren/aspnet:net11.0.0-rc.2