DEV Community

Cover image for The Size Paradox: 4 Techniques to Slim Down Windows Container Images
Maykol Alfaro
Maykol Alfaro

Posted on

The Size Paradox: 4 Techniques to Slim Down Windows Container Images

In our previous post, we discussed my experience working with Windows Containers, and why it was the most viable bridge to the cloud when trying to modernize your applications (.NET Framework).

Sometimes, in a world where Linux images are measured in megabytes, seeing a Windows build log report a 5GB image can be a bit shocking. For us, as software engineers, image size isn't just about disk space, it’s about deployment velocity. A 5GB image means slower cold starts in Kubernetes, longer CI/CD wait times, and higher egress costs. Here is how we managed to lower those numbers down by modifying our Dockerfiles.


The Base Image

One of the most important decisions is made in the very first line of the Dockerfile. Microsoft provides four primary images, and choosing the wrong one can cost you up to 4GB before you even add your code (Yes, that's true).

Base Image Typical Size Use Case
Nano Server ~100MB The "Alpine" equivalent of Windows. Built for the newest .NET versions (.NET 6+). No PowerShell, no WMI.
Server Core ~1.5GB - 2GB This is the best suite for .NET Framework 4.8. No GUI, but supports the full .NET stack and PowerShell.
Windows 5GB+ This is the largest image and has full Windows API support for workloads.
Windows Server 3GB+ It also provides the full Windows API support and allows you to use more server features. Use only if you have a strong dependency on any Windows specific feature such as GDI+ or specialized fonts.

Note: Always pin your version to the host OS LTSC (Long-Term Servicing Channel). For example, if your production nodes run Windows Server 2025, your FROM should be ltsc2025. Also, if your application for some reason has a strong dependency on a specific cumulative update you can also specify this in your FROM statement, so it should be like ltsc2025-KB5075899.


Multi-Stage Builds

If you are building a .NET Framework app, the SDK image (which includes MSBuild, NuGet, and compilers) is massive, often over 5GB. You must separate your build environment from your runtime environment. This can dramatically reduce the final image size.

# STAGE 1: The Builder
FROM mcr.microsoft.com/dotnet/framework/sdk:4.8 AS builder
WORKDIR /src
COPY . .
RUN nuget restore
RUN msbuild /p:Configuration=Release /p:OutputPath=/app/out

# STAGE 2: The Runtime
FROM mcr.microsoft.com/dotnet/framework/aspnet:4.8-windowsservercore-ltsc2025
WORKDIR /inetpub/wwwroot

# We only take the binaries. Everything else (SDK, caches, source) is discarded.
COPY --from=builder /app/out/_PublishedWebsites/MyLegacyApp .
Enter fullscreen mode Exit fullscreen mode

Layer "Squashing" and Atomic Commands

When you add a RUN command in your Dockerfile, it creates a new filesystem layer. In Windows, these layers are significantly heavier than in Linux.

The common mistake: Let's say you're running RUN powershell Expand-Archive app.zip and then RUN powershell Remove-Item app.zip. The ZIP file is still taking up space from the previous layer.

The fix: Use atomic commands. Download, unzip, install, and cleanup in one single layer. That way you're freeing some space and you won't end up with a huge image at the end of the process.

# Optimized Layering
RUN powershell -Command \
    $ProgressPreference = 'SilentlyContinue'; \
    Invoke-WebRequest -Uri https://example.com/installer.msi -OutFile installer.msi; \
    Start-Process msiexec.exe -ArgumentList '/i', 'installer.msi', '/quiet', '/norestart' -NoNewWindow -Wait; \
    Remove-Item installer.msi
Enter fullscreen mode Exit fullscreen mode

Mastering the .dockerignore

These .NET Framework projects are known for their "hidden" folders. Visual Studio creates bin/, obj/, .vs/, and packages/ folders that can easily add several hundred MBs to your build context.

If your build context is huge, your docker build command will hang for minutes just "Sending build context to Docker daemon."

How to solve this: Create a .dockerignore file and exclude everything except your source code. This is similar to how a .gitignore file works.

# .dockerignore
**/.git
**/.vs
**/bin
**/obj
**/packages
*.user
*.suo
Enter fullscreen mode Exit fullscreen mode

I know there might be more ways to optimize your Dockerfiles or your docker builds. In this section I just provided the most common use cases I faced during my "Lift-and-Shift" strategy working with this decade-old .NET Framework application, and how we approached them to successfully become the bridge we needed to get into the cloud.


Final Thoughts

Optimizing Windows images isn't just a nice-to-have feature, it’s what separates a hobbyist project from a production-ready enterprise system. By mastering multi-stage builds and atomic layer cleanup, we can turn a 10GB legacy monolithic application into a 2GB container that scales efficiently.

Top comments (0)