In this blog post Keep .NET App Running in Docker we will walk through how to containerise a .NET application, start it automatically when the container launches, and keep it running reliably.
Containers shine when you want consistent, repeatable runtime environments. Docker gives you a lightweight, portable unit to ship and run your .NET service anywhere.
High-level overview
Running a .NET app “all the time” in Docker means two things:
- Start it automatically when the container starts (using the image’s entrypoint).
- Keep it alive with sensible restart policies and health checks.
Docker images package your app and its runtime. Containers are running instances of those images. In each container, your application is PID 1—the primary foreground process. If it exits, the container stops. So the right way to keep a .NET app running is to launch the app in the foreground, let Docker manage the process lifecycle, and rely on Docker policies to restart the container on failures or host restarts.
The technology behind it
Several core Docker and .NET concepts make this work:
- Images and layers: Your Dockerfile defines steps to build an image. Each step creates a layer you can cache and reuse.
- ENTRYPOINT and CMD: These define what process runs when the container starts. For .NET, that’s usually
dotnet YourApp.dll
. - Foreground process model: Containers aren’t virtual machines. They run a single primary process in the foreground. When that process ends, the container stops.
- Signals and graceful shutdown: Docker sends
SIGTERM
thenSIGKILL
on stop. ASP.NET Core and .NET’s Generic Host respond toSIGTERM
and shut down gracefully by default. - Logging: Write logs to stdout/stderr. Docker captures these so you can use
docker logs
or forward them to a central system. - Health checks and restart policies: Health checks declare when your app is healthy. Restart policies tell Docker how to react when a container exits.
Create a minimal .NET web API
We’ll create a tiny ASP.NET Core app that exposes a health endpoint and listens on port 8080.
// Program.cs (.NET 8 minimal API)
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => Results.Ok(new { message = "Hello from Dockerized .NET" }));
app.MapGet("/health", () => Results.Ok("ok"));
app.Run();
Ensure your project file references .NET 8 LTS (or your target LTS):
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Build a production-friendly Docker image
Use a multi-stage Dockerfile to keep the final image small, create a non-root user, and expose port 8080. We’ll install curl
to support a simple in-container health check. If you prefer a leaner image, you can omit curl and rely on external checks.
# Dockerfile
# 1) Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# copy csproj and restore as a distinct layer for better caching
COPY ./WebDemo.csproj ./
RUN dotnet restore
# copy the rest and publish
COPY . ./
RUN dotnet publish -c Release -o /app/publish --no-restore
# 2) Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
# Optional: install curl for HEALTHCHECK
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
# Create unprivileged user
RUN useradd -m -u 10001 appuser
# Copy published artifacts
COPY --from=build /app/publish .
# Configure ASP.NET Core to listen on 8080
ENV ASPNETCORE_URLS=http://0.0.0.0:8080 \
DOTNET_EnableDiagnostics=0
EXPOSE 8080
# Simple health check hitting the /health endpoint
HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
CMD curl -f http://127.0.0.1:8080/health || exit 1
# Drop privileges
USER appuser
# Run the app in the foreground when the container starts
ENTRYPOINT ["dotnet", "WebDemo.dll"]
Key details:
- The ENTRYPOINT ensures your .NET app is PID 1 and runs in the foreground.
- USER appuser avoids running as root in production.
- HEALTHCHECK helps orchestrators know when to restart the container.
- ASPNETCORE_URLS binds to 0.0.0.0 so traffic from the host can reach the app.
Build and run the container
From the folder containing your Dockerfile and project:
# Build the image
docker build -t webdemo:1.0 .
# Run detached, publish port 8080, and auto-restart unless stopped
docker run -d \
--name webdemo \
-p 8080:8080 \
--restart unless-stopped \
webdemo:1.0
# Verify logs and test
docker logs -f webdemo
curl http://localhost:8080/
If the app exits unexpectedly, Docker restarts it due to the --restart unless-stopped
policy. For servers that reboot, the container comes back up automatically.
Keep it running the right way
Use Docker restart policies
--restart no
: default; don’t restart.--restart on-failure[:max-retries]
: restart on non-zero exit.--restart unless-stopped
: restart always unless you manually stop it.--restart always
: always restart—even after manual stop (less common for services you might stop by hand).
Expose a health endpoint
The /health
route we added is a simple way for Docker (and load balancers) to confirm the app is responsive. In real apps, use ASP.NET Core Health Checks to test dependencies like databases or queues.
Log to stdout and stderr
Don’t write logs to local files in the container. Use console logging so Docker can capture and rotate them. In production, ship them to a central logging system.
Graceful shutdown
.NET’s Generic Host handles SIGTERM
gracefully. If you need to run cleanup code on shutdown, hook into IHostApplicationLifetime
:
// Example: register shutdown callbacks
using Microsoft.Extensions.Hosting;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
lifetime.ApplicationStopping.Register(() => Console.WriteLine("Shutting down..."));
app.MapGet("/", () => "Hello");
app.Run();
Use Docker Compose for daily workflows
Compose lets you describe your app’s runtime settings in a file, then start/stop with one command. Great for dev and single-host deployments.
# docker-compose.yml
version: "3.8"
services:
web:
build: .
image: cloudproinc/webdemo:1.0
ports:
- "8080:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Production
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 30s
timeout: 3s
retries: 3
start_period: 20s
Run it with:
docker compose up -d
To view logs and status:
docker compose logs -f
docker compose ps
In production, Compose is fine for a single host. At scale, consider Kubernetes, Amazon ECS, or Azure Container Apps for orchestration, rolling updates, and auto-scaling.
Always-on background jobs with .NET Worker
If you’re building a long-running worker (not HTTP), use the .NET Worker template. It runs until cancelled, which is perfect for containers:
# Create a worker project
dotnet new worker -o WorkerDemo
# Program.cs (simplified)
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
Host.CreateDefaultBuilder(args)
.ConfigureLogging(lb => lb.AddSimpleConsole())
.ConfigureServices(services => services.AddHostedService<TimedWorker>())
.Build()
.Run();
public sealed class TimedWorker : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
Console.WriteLine($"Worker heartbeat: {DateTimeOffset.UtcNow}");
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
}
}
The Dockerfile pattern is the same—just set the ENTRYPOINT
to run the worker DLL. It will keep running until Docker sends SIGTERM
or the process fails.
Wrap-up
That’s the practical path to keep a .NET app running in Docker:
- Write a .NET app that runs in the foreground and exposes a health endpoint.
- Build a multi-stage image with an
ENTRYPOINT
that launches your app. - Run it with a sensible
--restart
policy so it stays up. - Add health checks and logs to stdout for observability.
- Use Docker Compose for convenient start/stop and configuration.
Follow these patterns and your .NET services will start cleanly, survive restarts, and run reliably—without hacks or heavyweight infrastructure.
Discover more from CPI Consulting
Subscribe to get the latest posts sent to your email.