Home C# / .NET Containerizing .NET Applications with Docker: A Practical Guide
Intermediate 6 min · July 13, 2026

Containerizing .NET Applications with Docker: A Practical Guide

Learn how to containerize .NET applications with Docker.

N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 13, 2026
last updated
2,043
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of .NET and C#
  • Docker Desktop or Docker Engine installed
  • .NET 6 SDK or later
  • Familiarity with command line
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Docker packages .NET apps with dependencies into lightweight containers for consistent deployment.
  • Use multi-stage builds to reduce image size by separating build and runtime.
  • Optimize Dockerfiles by copying only necessary files and using .dockerignore.
  • Debug containers using docker logs, exec, and attach commands.
  • Follow security best practices: run as non-root user, scan images, use minimal base images.
✦ Definition~90s read
What is Containerizing .NET Applications with Docker?

Containerizing .NET applications with Docker packages your app and its dependencies into a lightweight, portable container that runs consistently across any environment.

Imagine you're moving to a new house.
Plain-English First

Imagine you're moving to a new house. Instead of packing each item separately and hoping nothing breaks, you put all your furniture, clothes, and kitchenware into identical moving boxes that stack perfectly. Docker does the same for software: it packages your application with everything it needs (libraries, settings) into a standardized container that runs the same way on any computer, whether it's your laptop, a test server, or the cloud.

In modern software development, ensuring that an application runs consistently across different environments—developer laptops, staging servers, and production—is a constant challenge. The classic 'it works on my machine' problem wastes countless hours and causes production outages. Docker solves this by containerizing applications, packaging them with all dependencies into a lightweight, portable unit.

For .NET developers, Docker is especially powerful. Whether you're building an ASP.NET Core web API, a background service, or a console app, containerization simplifies deployment, scaling, and management. With the advent of .NET's cross-platform capabilities, Docker enables you to run .NET applications on Linux, Windows, or in the cloud with minimal friction.

This tutorial will guide you through the entire process of containerizing a .NET application. You'll learn how to write efficient Dockerfiles, use multi-stage builds to keep images small, debug containers in production, and follow security best practices. We'll also explore a real-world production incident caused by a Docker misconfiguration and how to avoid it. By the end, you'll be able to confidently containerize your .NET apps and deploy them anywhere.

1. Understanding Docker and .NET Integration

Docker is a platform for developing, shipping, and running applications inside lightweight containers. Containers are isolated environments that include the application and its dependencies, ensuring consistent behavior across different systems. For .NET developers, Docker provides a way to package ASP.NET Core applications, console apps, or services into portable images that can run on any Docker host.

The .NET SDK and runtime are available as official Docker images on Docker Hub (e.g., mcr.microsoft.com/dotnet/sdk:6.0 and mcr.microsoft.com/dotnet/aspnet:6.0). These images are optimized for different scenarios: the SDK image includes compilers and tools for building, while the runtime image is smaller and suitable for production.

Key benefits of containerizing .NET apps
  • Consistency: Same environment from development to production.
  • Isolation: Each container has its own filesystem, network, and processes.
  • Scalability: Easily scale out by running multiple containers.
  • CI/CD Integration: Build and test containers in pipelines.

Before diving in, ensure you have Docker installed (docker --version) and a .NET 6+ project ready.

Program.csCSHARP
1
2
3
4
5
6
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello from Docker!");

app.Run();
🔥Prerequisites
📊 Production Insight
Always use the official Microsoft .NET images from mcr.microsoft.com to ensure compatibility and security updates.
🎯 Key Takeaway
Docker containers package .NET apps with dependencies for consistent deployment across environments.

2. Writing Your First Dockerfile for a .NET App

A Dockerfile is a script that defines how to build a Docker image. For a .NET application, the Dockerfile typically uses a multi-stage build to keep the final image small. Let's create a Dockerfile for a simple ASP.NET Core Web API.

Start with the SDK image to restore dependencies and build the application. Then, use the runtime image to run the published output. This separation ensures that build tools are not included in the final image.

```dockerfile # Build stage FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /app

# Copy csproj and restore dependencies COPY *.csproj ./ RUN dotnet restore

# Copy everything else and build COPY . ./ RUN dotnet publish -c Release -o out

# Runtime stage FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS runtime WORKDIR /app COPY --from=build /app/out . ENTRYPOINT ["dotnet", "MyApp.dll"] ```

Explanation
  • FROM ... AS build: Uses the SDK image to build.
  • WORKDIR /app: Sets working directory.
  • COPY *.csproj ./: Copies only the project file to leverage Docker layer caching.
  • RUN dotnet restore: Restores NuGet packages.
  • COPY . ./: Copies the rest of the source code.
  • RUN dotnet publish: Builds and publishes the app.
  • FROM ... AS runtime: Uses the smaller runtime image.
  • COPY --from=build /app/out .: Copies published output from build stage.
  • ENTRYPOINT: Specifies the command to run when container starts.

Build the image with: docker build -t myapp . Run the container: docker run -p 8080:80 myapp

Now your app is accessible at http://localhost:8080.

DockerfileDOCKERFILE
1
2
3
4
5
6
7
8
9
10
11
12
13
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /app

COPY *.csproj ./
RUN dotnet restore

COPY . ./
RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS runtime
WORKDIR /app
COPY --from=build /app/out .
ENTRYPOINT ["dotnet", "MyApp.dll"]
💡Layer Caching
📊 Production Insight
Use the '--no-restore' flag in publish if you already restored in a separate step to avoid redundant network calls.
🎯 Key Takeaway
Multi-stage builds separate build and runtime, resulting in smaller, more secure images.

3. Optimizing Docker Images for .NET

Image size matters for fast deployments and reduced storage costs. Here are key optimization techniques:

  1. Use .dockerignore: Exclude files not needed in the build context, like node_modules, .git, bin/obj folders. Example:
  2. ```
  3. **/.classpath
  4. **/.dockerignore
  5. **/.env
  6. **/.git
  7. **/.gitignore
  8. **/.project
  9. **/.settings
  10. **/.toolstarget
  11. **/.vs
  12. **/.vscode
  13. */.*proj.user
  14. */.dbmdl
  15. */.jfm
  16. **/bin
  17. **/charts
  18. */docker-compose
  19. */compose
  20. */Dockerfile
  21. **/node_modules
  22. **/npm-debug.log
  23. **/obj
  24. **/secrets.dev.yaml
  25. **/values.dev.yaml
  26. ```
  27. Use smaller base images: For runtime, use aspnet:6.0-alpine instead of aspnet:6.0. Alpine-based images are significantly smaller (around 100MB vs 200MB).
  28. Remove unnecessary packages: In the build stage, you can use --self-contained false and --no-restore to minimize output.
  29. Use assembly trimming: For self-contained deployments, enable trimming to remove unused code. Add <PublishTrimmed>true</PublishTrimmed> to your csproj.
  30. Combine RUN commands: In Dockerfile, combine multiple RUN commands with && to reduce layers.

Example optimized Dockerfile: ```dockerfile FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build WORKDIR /app COPY *.csproj . RUN dotnet restore COPY . . RUN dotnet publish -c Release -o out --no-restore

FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine AS runtime WORKDIR /app COPY --from=build /app/out . ENTRYPOINT ["dotnet", "MyApp.dll"] ```

This reduces image size from ~200MB to ~100MB.

Dockerfile.optimizedDOCKERFILE
1
2
3
4
5
6
7
8
9
10
11
FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build
WORKDIR /app
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o out --no-restore

FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine AS runtime
WORKDIR /app
COPY --from=build /app/out .
ENTRYPOINT ["dotnet", "MyApp.dll"]
⚠ Alpine Compatibility
📊 Production Insight
Regularly scan your images with tools like Docker Scout or Trivy to identify vulnerabilities and unnecessary packages.
🎯 Key Takeaway
Optimizing image size improves deployment speed and reduces resource usage.

4. Debugging .NET Containers in Production

Debugging a containerized application in production requires different tools than local debugging. Here are essential techniques:

1. Viewing Logs: Use docker logs <container> to see stdout/stderr. For real-time logs, add -f flag.

2. Exec into Container: docker exec -it <container> /bin/sh gives you a shell inside the container. From there, you can inspect environment variables, files, and run diagnostic commands.

3. Attach a Debugger: For .NET, you can attach a remote debugger using docker exec and dotnet-dump or dotnet-gcdump for memory analysis. Install the dotnet tools globally: ``bash dotnet tool install --global dotnet-dump dotnet tool install --global dotnet-gcdump ` Then inside the container: `bash dotnet-dump collect --process-id 1 dotnet-gcdump collect --process-id 1 ``

4. Health Checks: Add health checks to your container to automatically detect failures. In Dockerfile, add: ``dockerfile HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD curl -f http://localhost/health || exit 1 ``

5. Use Environment Variables: Pass configuration via environment variables (e.g., -e ASPNETCORE_ENVIRONMENT=Development) to toggle debug logging.

6. Resource Monitoring: Use docker stats <container> to monitor CPU and memory usage in real-time.

Example: Debugging a high memory issue: ``bash docker exec -it mycontainer /bin/sh # Inside container dotnet-gcdump collect -p 1 -o /tmp/dump.gcdump exit docker cp mycontainer:/tmp/dump.gcdump ./dump.gcdump # Analyze locally with dotnet-gcdump report ``

debug.shBASH
1
2
3
4
5
6
7
8
docker exec -it mycontainer /bin/sh
# Inside container:
dotnet-gcdump collect -p 1 -o /tmp/dump.gcdump
exit
# Copy dump to host:
docker cp mycontainer:/tmp/dump.gcdump ./dump.gcdump
# Analyze:
dotnet-gcdump report ./dump.gcdump
Output
Writing gcdump to file...
Successfully wrote /tmp/dump.gcdump
🔥Debugging with Visual Studio
📊 Production Insight
Always include diagnostic tools in your production image? No, install them on-demand via docker exec to keep images minimal.
🎯 Key Takeaway
Use docker exec, dotnet-dump, and health checks to debug containers in production.

5. Security Best Practices for .NET Docker Containers

Security is critical when running containers in production. Follow these best practices:

1. Run as Non-Root User: By default, containers run as root. Create a non-root user in the Dockerfile: ``dockerfile RUN adduser -u 1000 --disabled-password appuser USER appuser ``

2. Use Minimal Base Images: Prefer aspnet:6.0-alpine over full images to reduce attack surface.

3. Scan Images for Vulnerabilities: Use Docker Scout, Trivy, or Snyk to scan images. Integrate into CI/CD.

4. Avoid Secrets in Images: Never hardcode passwords or connection strings. Use Docker secrets or environment variables at runtime.

5. Keep Images Updated: Regularly rebuild images with latest base images to get security patches.

6. Limit Container Capabilities: Use --cap-drop=ALL and add only necessary capabilities.

7. Use Read-Only Filesystem: Run containers with --read-only flag to prevent modifications.

8. Enable Content Trust: Use Docker Content Trust to verify image signatures.

Example secure Dockerfile: ```dockerfile FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build WORKDIR /app COPY *.csproj . RUN dotnet restore COPY . . RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine AS runtime WORKDIR /app COPY --from=build /app/out . RUN adduser -u 1000 --disabled-password appuser USER appuser ENV ASPNETCORE_URLS=http://+:8080 EXPOSE 8080 ENTRYPOINT ["dotnet", "MyApp.dll"] ```

Run with: ``bash docker run -d --read-only --cap-drop=ALL --security-opt=no-new-privileges -p 8080:8080 myapp ``

Dockerfile.secureDOCKERFILE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build
WORKDIR /app
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine AS runtime
WORKDIR /app
COPY --from=build /app/out .
RUN adduser -u 1000 --disabled-password appuser
USER appuser
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.dll"]
⚠ Read-Only Filesystem
📊 Production Insight
Combine read-only filesystem with non-root user to mitigate many common attacks.
🎯 Key Takeaway
Run containers as non-root, use minimal images, and scan for vulnerabilities to secure your .NET apps.

6. Orchestrating .NET Containers with Docker Compose

Docker Compose allows you to define and run multi-container applications. For a .NET app that depends on a database, you can define services in a docker-compose.yml file.

Example: ASP.NET Core API with PostgreSQL.

``yaml version: '3.8' services: api: build: context: . dockerfile: Dockerfile ports: - "5000:80" environment: - ASPNETCORE_ENVIRONMENT=Production - ConnectionStrings__DefaultConnection=Host=db;Database=mydb;Username=postgres;Password=postgres depends_on: - db db: image: postgres:14-alpine environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: mydb volumes: - pgdata:/var/lib/postgresql/data volumes: pgdata: ``

Run with: docker-compose up -d

This starts both the API and database containers. The API can connect to the database using the hostname 'db' (the service name).

For development, you can mount your source code as a volume to enable hot reload: ``yaml services: api: build: context: . dockerfile: Dockerfile.dev volumes: - .:/app environment: - ASPNETCORE_ENVIRONMENT=Development ``

Docker Compose is ideal for local development and testing, but for production, consider Kubernetes or Docker Swarm for orchestration.

docker-compose.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
version: '3.8'
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "5000:80"
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ConnectionStrings__DefaultConnection=Host=db;Database=mydb;Username=postgres;Password=postgres
    depends_on:
      - db
  db:
    image: postgres:14-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data
volumes:
  pgdata:
💡Connection Strings
📊 Production Insight
For production, use secrets management (e.g., Docker secrets) instead of environment variables for sensitive data.
🎯 Key Takeaway
Docker Compose simplifies running multi-container .NET applications with dependencies.

7. CI/CD Pipeline Integration for .NET Docker Images

Integrating Docker into your CI/CD pipeline ensures that your images are built, tested, and deployed automatically. Here's an example using GitHub Actions.

```yaml name: Build and Push Docker Image

on: push: branches: [ main ]

jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Log in to Docker Hub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push uses: docker/build-push-action@v4 with: context: . push: true tags: myusername/myapp:latest ```

For .NET-specific optimizations, you can also run unit tests inside the build stage:

``dockerfile FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /app COPY . . RUN dotnet test --logger trx --results-directory /testresults RUN dotnet publish -c Release -o out ``

Then in the pipeline, you can publish test results.

Key considerations
  • Use Docker layer caching to speed up builds.
  • Scan images for vulnerabilities before pushing.
  • Tag images with commit SHA for traceability.

Example pipeline with caching: ``yaml - name: Build and push uses: docker/build-push-action@v4 with: context: . push: true tags: myusername/myapp:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max ``

.github/workflows/docker-build.ymlYAML
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
26
name: Build and Push Docker Image

on:
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      - name: Log in to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: myusername/myapp:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max
🔥GitHub Actions Cache
📊 Production Insight
Always tag images with a unique identifier (commit SHA) to enable rollbacks and traceability.
🎯 Key Takeaway
Automate Docker image builds in CI/CD to ensure consistency and enable rapid deployments.
● Production incidentPOST-MORTEMseverity: high

The Case of the Bloated Container: A 2GB Image That Crashed Production

Symptom
Deployments took over 10 minutes, and production nodes ran out of disk space, causing containers to crash.
Assumption
The developer assumed that using the default 'dotnet publish' output directly in a container was fine, and that Docker would optimize image size automatically.
Root cause
The Dockerfile used a single-stage build that included the entire SDK and all build artifacts, resulting in a 2GB image. Additionally, the Dockerfile copied the entire project directory, including node_modules and test files, into the image.
Fix
Switched to a multi-stage build: first stage used the SDK to build and publish, second stage used the ASP.NET runtime image and copied only the published output. Added a .dockerignore file to exclude unnecessary files. Image size dropped to 200MB.
Key lesson
  • Always use multi-stage builds to separate build and runtime environments.
  • Use .dockerignore to exclude files not needed in the final image (e.g., node_modules, .git, test projects).
  • Choose the smallest appropriate base image (e.g., aspnet:6.0-alpine instead of sdk).
  • Regularly scan images for vulnerabilities and unnecessary packages.
  • Monitor image sizes in CI/CD pipelines to catch regressions early.
Production debug guideSymptom to Action4 entries
Symptom · 01
Container exits immediately after start
Fix
Check logs with 'docker logs <container>' and ensure the application is configured to listen on the correct port (e.g., ASPNETCORE_URLS).
Symptom · 02
Application runs but returns 502 Bad Gateway
Fix
Verify that the container's exposed port matches the port in the reverse proxy configuration. Use 'docker port <container>' to see port mappings.
Symptom · 03
High memory usage or slow response
Fix
Attach to the container with 'docker exec -it <container> /bin/bash' and use 'top' or 'dotnet-counters' to profile. Check for memory leaks in the application.
Symptom · 04
Cannot connect to database from container
Fix
Ensure the database host is reachable (use container name or IP, not 'localhost'). Check network mode and environment variables for connection strings.
★ Quick Debug Cheat Sheet for Docker .NET ContainersCommon symptoms and immediate actions for containerized .NET apps.
Container exits immediately
Immediate action
Check logs
Commands
docker logs <container>
docker inspect <container>
Fix now
Set ASPNETCORE_URLS=http://+:80
Port not accessible+
Immediate action
Verify port mapping
Commands
docker port <container>
docker ps
Fix now
Use -p 8080:80 to map host port 8080 to container port 80
High memory usage+
Immediate action
Check resource stats
Commands
docker stats <container>
docker exec -it <container> dotnet-counters monitor
Fix now
Add memory limits in docker run --memory=512m
Database connection failure+
Immediate action
Check connection string
Commands
docker exec -it <container> env
docker network inspect bridge
Fix now
Use container name as host, e.g., 'Server=db;Database=mydb;...'
FeatureSingle-Stage BuildMulti-Stage Build
Image Size~2GB (includes SDK)~200MB (runtime only)
Build SpeedSlower (no caching)Faster with layer caching
SecurityLarger attack surfaceMinimal attack surface
ComplexitySimpleModerate
Production ReadyNoYes
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
Program.csvar builder = WebApplication.CreateBuilder(args);1. Understanding Docker and .NET Integration
DockerfileFROM mcr.microsoft.com/dotnet/sdk:6.0 AS build2. Writing Your First Dockerfile for a .NET App
Dockerfile.optimizedFROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build3. Optimizing Docker Images for .NET
debug.shdocker exec -it mycontainer /bin/sh4. Debugging .NET Containers in Production
Dockerfile.secureFROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build5. Security Best Practices for .NET Docker Containers
docker-compose.ymlversion: '3.8'6. Orchestrating .NET Containers with Docker Compose
.githubworkflowsdocker-build.ymlname: Build and Push Docker Image7. CI/CD Pipeline Integration for .NET Docker Images

Key takeaways

1
Multi-stage builds are essential for creating small, secure .NET Docker images.
2
Always run containers as a non-root user and follow security best practices.
3
Use Docker Compose for local development and Kubernetes for production orchestration.
4
Integrate Docker builds into CI/CD pipelines for automated, consistent deployments.
5
Debug containers using docker exec, dotnet-dump, and health checks.

Common mistakes to avoid

5 patterns
×

Using a single-stage Dockerfile that includes the SDK in the final image.

×

Hardcoding connection strings or secrets in the Dockerfile or appsettings.json.

×

Running the container as root without a non-root user.

×

Not using .dockerignore and copying entire project directory including node_modules and bin/obj.

×

Assuming the container can access localhost for a database running on the host.

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain multi-stage builds in Docker and why they are important for .NET...
Q02SENIOR
How would you debug a memory leak in a .NET container running in product...
Q03SENIOR
What are the security best practices for running .NET applications in Do...
Q04SENIOR
How does Docker layer caching work and how can you optimize a .NET Docke...
Q05JUNIOR
What is the difference between docker-compose and Kubernetes for .NET co...
Q01 of 05SENIOR

Explain multi-stage builds in Docker and why they are important for .NET applications.

ANSWER
Multi-stage builds use multiple FROM statements in a Dockerfile. The first stage (build) uses the SDK image to restore dependencies and compile the app. The second stage (runtime) uses a smaller runtime image and copies only the published output. This reduces the final image size by excluding build tools and intermediate files, leading to faster deployments and smaller attack surface.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between Docker image and container?
02
How do I pass configuration to a .NET container?
03
Can I run Windows containers with .NET?
04
How do I debug a .NET container locally with Visual Studio?
05
What is the best base image for production .NET apps?
N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 13, 2026
last updated
2,043
articles · all by Naren
🔥

That's ASP.NET. Mark it forged?

6 min read · try the examples if you haven't

Previous
CI/CD for .NET with GitHub Actions
30 / 35 · ASP.NET
Next
Dapper Micro-ORM in .NET