Introduction: Why Advanced Level?
\nContainerization of C# applications based on .NET is no longer just a trend — it is an industry standard. At the beginner level, a basic Dockerfile with FROM mcr.microsoft.com/dotnet/aspnet is sufficient. However, in a production environment, tasks arise that require a deeper understanding: minimizing image size, security, multi-container architecture, efficient configuration management, and debugging.
This article is intended for developers who are already familiar with the basics of Docker and C# but want to move to the next level. We will cover advanced techniques that allow you to create lightweight, secure, and easily scalable images for your .NET applications.
\n\n1. Multi-stage Build: The Art of Lightweight Images
\nThe classic problem is that .NET SDK images weigh over 1.5 GB. Bringing them into production is pointless and dangerous. Multi-stage builds solve this problem by separating the build stage from the runtime stage.
\n\nAdvanced Dockerfile with Multi-stage
\nInstead of simply copying binary files, we use official SDK images for compilation and then copy the result into a minimal aspnet image.
# Stage 1: Build\nFROM mcr.microsoft.com/dotnet/sdk:8.0 AS build\nWORKDIR /app\n\n# Copy only project files (cache optimization)\nCOPY src/MyApp.csproj .\nRUN dotnet restore\n\n# Copy the rest of the code\nCOPY src/ .\nRUN dotnet publish -c Release -o /app/publish\n\n# Stage 2: Runtime\nFROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime\nWORKDIR /app\n\n# Add a non-privileged user\nRUN adduser --disabled-password --gecos '' appuser\nUSER appuser\n\nCOPY --from=build /app/publish .\n\nEXPOSE 8080\nENV ASPNETCORE_URLS=http://+:8080\nENTRYPOINT ["dotnet", "MyApp.dll"]\nWhat's important here:
\n- \n
- Layer caching: First, only
.csprojis copied anddotnet restoreis executed. As long as dependencies don't change, Docker will use the cache for this layer. \n - Non-privileged user:
appuser— critically important for security in production. \n - Port 8080: In .NET 8, port 8080 is used by default, which aligns with modern cloud standards. \n
Trick: Trimmed and AOT Build
\nTo further reduce size, use Trimming (removing unused code) or Native AOT. This allows you to get images as small as 10-30 MB.
\n# For ReadyToRun (R2R) and Trimming\nRUN dotnet publish -c Release -r linux-x64 --self-contained true \\\n -p:PublishTrimmed=true \\\n -p:PublishSingleFile=true \\\n -o /app/publish\nImportant: AOT imposes limitations on reflection and dynamic assembly loading. Before using it, ensure your code is compatible.
\n\n2. Working with Configuration and Secrets
\nStoring connection strings and API keys inside the image is a critical mistake. At the advanced level, we use Docker Secrets, environment variables, and integration with appsettings.json.
Overriding appsettings via Docker
\n.NET has a built-in configuration priority mechanism. We can pass settings through environment variables, which are automatically mapped to configuration sections.
\n// In C# code\nvar builder = WebApplication.CreateBuilder(args);\nbuilder.Configuration\n .AddJsonFile("appsettings.json", optional: false)\n .AddEnvironmentVariables(); // Overrides values from JSON\nRun the container:
\ndocker run -d -p 8080:8080 \\\n -e "ConnectionStrings__DefaultConnection=Server=db;Database=myapp;User Id=sa;Password=MyPass123!" \\\n -e "Logging__LogLevel__Default=Warning" \\\n