You can run C# .NET apps in production using Docker, but the real magic isn’t just packaging; it’s about transforming how you think about deployment, scaling, and resilience.

Let’s see this in action. Imagine a simple ASP.NET Core web API. Normally, you’d deploy this to an IIS server, manage its lifecycle, and pray it doesn’t crash. With Docker, we’re going to package it into an immutable image and run it as a container.

First, the Dockerfile:

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build AS publish
WORKDIR /app
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=publish /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "YourAppName.dll"]

This Dockerfile uses a multi-stage build. The first stage (build) uses the .NET SDK to compile and publish your application. This creates a lean runtime image in the second stage (runtime) that only contains the necessary files to run your app, making the final image significantly smaller and more secure. We EXPOSE 8080 because that’s the default port ASP.NET Core listens on within the container, and ENTRYPOINT specifies how to start your application.

To build this image, you’d navigate to your project directory in the terminal and run:

docker build -t my-dotnet-app:1.0 .

This command tells Docker to build an image using the Dockerfile in the current directory (.) and tag it as my-dotnet-app with version 1.0.

Now, to run it:

docker run -d -p 80:8080 --name my-running-app my-dotnet-app:1.0

Here, -d runs the container in detached mode (in the background), -p 80:8080 maps port 80 on your host machine to port 8080 inside the container (where your app is listening), and --name my-running-app gives your container a recognizable name. You can now access your application by navigating to http://localhost in your browser.

The problem this solves is the "it works on my machine" syndrome. Docker containers encapsulate your application and its dependencies, ensuring a consistent environment from development to production. This dramatically reduces deployment failures and simplifies environment management. Instead of meticulously configuring servers, you’re managing containers, which are lightweight, portable, and disposable.

Internally, Docker creates isolated environments. When you run a container, it gets its own filesystem, network interface, and process space. The base image you start with provides the operating system and .NET runtime. Your application code and its specific dependencies are layered on top. When you publish your .NET app, you’re generating a self-contained deployment that includes the .NET runtime if you’re not using a framework-dependent deployment. The aspnet:8.0 image already has the .NET 8 runtime, so we only need to copy our published app binaries.

The exact levers you control are primarily in your Dockerfile. You choose the base image (e.g., mcr.microsoft.com/dotnet/sdk:8.0 vs. mcr.microsoft.com/dotnet/aspnet:8.0), how you copy your build artifacts, the ENTRYPOINT command, and environment variables. For production, you’ll also be concerned with optimizing the image size by using slim base images and multi-stage builds, and securing the image by running as a non-root user. You can also configure health checks to ensure your container is actually running correctly.

The most surprising thing is that the ENTRYPOINT command in your Dockerfile is not just a startup script; it’s the primary process that runs inside the container. If that process exits, the container stops. This is why simple CMD directives that just run a shell command might not be what you want for a long-running service; the shell process would exit, and so would your container. You need to ensure your ENTRYPOINT is a process that keeps running, like your ASP.NET Core application.

When you’re ready to scale, you’ll move beyond single-host Docker and look into orchestration platforms like Kubernetes or Docker Swarm.

Want structured learning?

Take the full Csharp course →