Windows Containers
Windows containers run Windows binaries against a Windows kernel — they are a genuinely different thing from running Docker on Windows, which is what Docker Desktop does by default (Linux containers inside a WSL2 VM). If you need to containerize a .NET Framework application, an IIS site, or anything depending on Windows APIs, you need Windows containers and a Windows host.
The two constraints that shape everything else: a Windows container cannot run on a Linux host, and the host build version constrains which container images will run.
Table of Contents
- When You Need Them
- Isolation Modes
- Version Compatibility
- Base Images
- Host Setup
- Switching Container Types
- Writing Windows Dockerfiles
- Networking
- Storage
- Resource Limits
- Troubleshooting
When You Need Them
| Workload | Container type |
|---|---|
| .NET Framework (4.x) application | Windows |
| Classic ASP.NET on IIS | Windows |
| Windows Services, COM components, WCF | Windows |
| Software requiring the Windows registry or Win32 APIs | Windows |
| .NET (Core) 6/8/9+ cross-platform application | Linux — smaller, cheaper, better tooled |
| Anything new | Linux, unless a hard Windows dependency exists |
Windows containers carry real costs: images are one to two orders of magnitude larger, the host needs Windows licensing, orchestration support is thinner, and the version-matching rules below add operational friction. Porting to .NET 8 and Linux containers is usually the better investment when it is feasible. Where it is not — a decade-old .NET Framework application that must keep running — Windows containers are a sound way to get it off a hand-built server.
Isolation Modes
This is the central concept, and the source of most confusion.
Process Isolation
Containers share the host kernel, exactly as Linux containers do. Namespaces and job objects provide separation.
- Fast startup, low overhead, higher density
- Requires the container's base image build to match the host's
- The default on Windows Server
Hyper-V Isolation
Each container runs in a minimal, purpose-built virtual machine with its own kernel.
- Startup measured in seconds rather than sub-second; noticeably more memory per container
- Any supported base image version runs on any supported host
- Stronger security boundary — a kernel exploit is contained by the hypervisor
- The default on Windows 10/11 client editions
# Explicit selection at run time
docker run --isolation=process mcr.microsoft.com/windows/servercore:ltsc2022 cmd /c echo hi
docker run --isolation=hyperv mcr.microsoft.com/windows/servercore:ltsc2019 cmd /c echo hi
# Check what a running container used
docker inspect -f '{{.HostConfig.Isolation}}' mycontainer
Set a host default in daemon.json:
{
"exec-opts": ["isolation=hyperv"]
}
Note
Hyper-V isolation requires hardware virtualization and the Hyper-V feature. In a virtual machine this means nested virtualization must be enabled by the hypervisor — a common blocker on cloud VMs and on vSphere without explicit configuration.
Choosing
| Priority | Mode |
|---|---|
| Density and speed, images match host | Process |
| Running mixed or older base image versions | Hyper-V |
| Untrusted or multi-tenant workloads | Hyper-V |
| Development on Windows 10/11 | Hyper-V (usually the only option) |
Version Compatibility
Under process isolation, the container's kernel is the host's kernel, so the base image must match the host build. Mismatches fail to start, or — worse across revisions — behave unpredictably.
| Host | Process isolation supports | Hyper-V isolation supports |
|---|---|---|
| Windows Server 2019 (LTSC) | ltsc2019 images |
ltsc2019 and earlier |
| Windows Server 2022 (LTSC) | ltsc2022 images |
ltsc2022 and earlier |
| Windows Server 2025 (LTSC) | ltsc2025 images |
ltsc2025 and earlier |
| Windows 10/11 client | Matching build only | All supported versions |
# Host build
[System.Environment]::OSVersion.Version
Get-ComputerInfo | Select-Object WindowsProductName, OsBuildNumber
# Base image build
docker inspect mcr.microsoft.com/windows/servercore:ltsc2022 `
--format '{{.OsVersion}}'
Rules of thumb:
- The host must be the same or newer than the container base image. An
ltsc2022image will not run on a 2019 host in any mode. - Older images on a newer host require Hyper-V isolation. An
ltsc2019image runs on a 2022 host only with--isolation=hyperv. - Pin base image tags to an LTSC version, never
:latest. Alatestthat rolls to a new LTSC release silently breaks every process-isolated host that has not been upgraded.
Base Images
All published under mcr.microsoft.com:
| Image | Approx. size | Contains |
|---|---|---|
windows/nanoserver |
250–300 MB | Minimal. No PowerShell, no full .NET Framework. cmd only. |
windows/servercore |
2–5 GB | Most of the Windows Server API surface, PowerShell, IIS-capable |
windows |
8+ GB | Full API set, including graphics and media. Rarely needed. |
docker pull mcr.microsoft.com/windows/nanoserver:ltsc2022
docker pull mcr.microsoft.com/windows/servercore:ltsc2022
Application images build on these:
# .NET Framework runtime and SDK
docker pull mcr.microsoft.com/dotnet/framework/runtime:4.8-windowsservercore-ltsc2022
docker pull mcr.microsoft.com/dotnet/framework/sdk:4.8-windowsservercore-ltsc2022
# ASP.NET (Framework) with IIS preconfigured
docker pull mcr.microsoft.com/dotnet/framework/aspnet:4.8-windowsservercore-ltsc2022
# .NET (Core) on Nano Server — much smaller
docker pull mcr.microsoft.com/dotnet/aspnet:8.0-nanoserver-ltsc2022
Important
Nano Server dropped PowerShell some releases ago. If a Dockerfile uses
RUN powershell ... against nanoserver, it fails — use servercore for the build stage
and copy artifacts into nanoserver for runtime, the same multi-stage pattern used on
Linux.
Host Setup
Windows Server
# Install the Containers feature
Install-WindowsFeature -Name Containers
Restart-Computer -Force
# Hyper-V, only if you need Hyper-V isolation
Install-WindowsFeature -Name Hyper-V -IncludeManagementTools
Restart-Computer -Force
Install a container runtime. Microsoft partners with Mirantis for the supported Docker engine on Windows Server:
Install-Module -Name DockerMsftProvider -Repository PSGallery -Force
Install-Package -Name docker -ProviderName DockerMsftProvider -Force
Restart-Computer -Force
docker version
docker info
Note
DockerMsftProvider is the long-standing path but has been superseded — current guidance
is the Mirantis Container Runtime installer, and Microsoft's own tooling increasingly
targets containerd directly. Check the current Microsoft documentation for your Server
release rather than assuming the PowerShell provider is still the supported route.
Windows 10/11
Docker Desktop with Hyper-V or WSL2 backend. Windows containers require the Hyper-V backend and Pro, Enterprise, or Education — Home editions cannot run them.
# Enable required features
Enable-WindowsOptionalFeature -Online -FeatureName Containers -All
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All
See Installing Docker for the general Docker Desktop install.
Switching Container Types
A single daemon serves either Linux or Windows containers, not both simultaneously.
# Docker Desktop — right-click the tray icon → "Switch to Windows containers…"
# Or from the CLI:
& $Env:ProgramFiles\Docker\Docker\DockerCli.exe -SwitchDaemon
# Confirm which mode is active
docker info --format '{{.OSType}}'
# windows or linux
Switching restarts the daemon. Containers of the other type keep existing but are not
listed or runnable until you switch back — docker ps -a showing an empty list after a
switch is expected, not data loss.
Tip
On a build agent that needs both, run two hosts rather than switching. Switching is slow, disruptive, and awkward to automate reliably.
Writing Windows Dockerfiles
# escape=`
FROM mcr.microsoft.com/dotnet/framework/aspnet:4.8-windowsservercore-ltsc2022
SHELL ["powershell", "-Command", "$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue';"]
WORKDIR /inetpub/wwwroot
RUN Install-WindowsFeature Web-Asp-Net45
COPY ./publish/ .
EXPOSE 80
The escape Directive
Windows paths use backslashes, which Dockerfile treats as line continuations. The
# escape= directive on the first line switches the continuation character to a backtick:
# escape=`
FROM mcr.microsoft.com/windows/servercore:ltsc2022
RUN powershell -Command `
New-Item -ItemType Directory -Path C:\app; `
Write-Host 'created'
Without it, use forward slashes in paths — Windows accepts them in most contexts — and keep
the default \ continuation.
SHELL and Error Handling
The default shell is cmd /S /C, which does not stop on error the way you would expect.
Set PowerShell with $ErrorActionPreference='Stop' so a failing command actually fails the
build:
SHELL ["powershell", "-Command", "$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue';"]
$ProgressPreference='SilentlyContinue' is not cosmetic — PowerShell's progress bar makes
Invoke-WebRequest dramatically slower in a non-interactive session.
Multi-Stage Builds
# escape=`
FROM mcr.microsoft.com/dotnet/framework/sdk:4.8-windowsservercore-ltsc2022 AS build
WORKDIR /src
COPY *.sln .
COPY MyApp/*.csproj MyApp/
RUN nuget restore
COPY . .
RUN msbuild /p:Configuration=Release /p:DeployOnBuild=true /p:PublishUrl=C:\publish
FROM mcr.microsoft.com/dotnet/framework/aspnet:4.8-windowsservercore-ltsc2022 AS runtime
WORKDIR /inetpub/wwwroot
COPY --from=build /publish .
The same caching and ordering principles from Building Images apply — copy project files and restore packages before copying source.
Users
Windows containers have no direct USER 1000 equivalent. Built-in accounts are available:
USER ContainerUser # unprivileged
USER ContainerAdministrator # administrative
ContainerUser should be the default for anything not needing to modify the system. IIS
images typically run the worker process under its own application pool identity regardless.
Networking
Windows uses the Host Network Service, with different drivers from Linux:
| Driver | Purpose |
|---|---|
nat |
Default. Private subnet behind NAT — the equivalent of bridge. |
transparent |
Container attaches directly to the physical network, like macvlan. |
l2bridge |
Layer 2 bridging with the host MAC; for SDN environments. |
overlay |
Multi-host, Swarm or Kubernetes. |
ics |
Internet Connection Sharing, client SKUs only. |
docker network ls
docker network create -d nat --subnet=172.30.0.0/16 --gateway=172.30.0.1 appnet
docker network create -d transparent transparent-net
Behavioral differences worth knowing:
There is no
--network host. Windows has no equivalent; publish ports instead.Published ports may not be reachable via
localhostfrom the host. Historically you had to use the container's IP directly. This has improved in recent Windows builds, but iflocalhost:8080fails, try the container address:docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mycontainerDNS behaves like Linux user-defined networks — containers on a custom
natnetwork resolve each other by name.
See Docker Networking for the concepts these mirror.
Storage
Volumes and bind mounts work, with Windows path syntax:
docker volume create appdata
docker run -d -v appdata:C:\data myapp
# Bind mount — note the quoting
docker run -d -v C:\host\config:C:\app\config myapp
docker run -d --mount type=bind,source=C:\host\config,target=C:\app\config myapp
Constraints that differ from Linux:
- You cannot bind-mount a single file, only directories. Mount the parent directory.
tmpfsmounts are not supported.- SMB shares can be mounted, but require credentials configured for the container's identity — often the awkward part of migrating a file-dependent application.
- The storage layer uses the Windows filter driver, not overlay2.
docker inforeportswindowsfilter.
Resource Limits
docker run -d --memory 2g --cpus 2 myapp
# Windows-specific CPU controls
docker run -d --cpu-count 2 --cpu-percent 50 myapp
Under Hyper-V isolation, --memory sets the VM's memory and is a hard allocation reserved up
front — sizing it generously across many containers exhausts host memory faster than the
equivalent Linux configuration. Under process isolation it behaves as a job object limit,
much like Linux cgroups.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
image operating system "windows" cannot be used on this platform |
Daemon is in Linux mode; switch it |
The container operating system does not match the host |
Base image build newer than host, or process isolation with a mismatch — use --isolation=hyperv or a matching tag |
hcsshim::CreateComputeSystem failures |
Hyper-V not enabled, or nested virtualization unavailable in the VM |
RUN powershell fails on nanoserver |
No PowerShell in that image; build on servercore |
Published port unreachable on localhost |
Use the container IP; see Networking |
Extremely slow Invoke-WebRequest in build |
Set $ProgressPreference='SilentlyContinue' |
| Backslash paths breaking the Dockerfile | Add `# escape=`` or use forward slashes |
| Enormous image sizes | Expected — use nanoserver runtime stages and multi-stage builds |
# Which mode is the daemon in
docker info --format '{{.OSType}} / {{.Isolation}}'
# Host and image build numbers side by side
Get-ComputerInfo | Select-Object OsBuildNumber
docker image inspect <image> --format '{{.OsVersion}}'
# Container logs and interactive access
docker logs mycontainer
docker exec -it mycontainer powershell
Licensing
Windows containers require Windows Server licensing on the host. Under process isolation, containers are covered by the host license; Hyper-V-isolated containers may count against virtualization rights depending on the edition. Confirm the terms for your specific agreement before planning density — this is a real cost input, not a formality.
Related Topics
- Installing Docker — Docker Desktop on Windows
- Building Images — multi-stage builds and layer caching
- Docker Networking — the Linux concepts these mirror
- Docker Storage — volume and bind mount fundamentals
- Docker Swarm — mixed Linux/Windows clusters
- Windows Infrastructure — Windows Server administration