Windows Containers - Docker Introduction
Note that absolutely none of this is authoritative or directly based on relevant documentation. It is mostly what I found and figured out and guessed and (in some cases) made up. Some of it may be wrong or dangerous or lead to disaster or confusion. I am not taking responsibility here for anything, not even spelling or the weather. Read and follow at your own peril! Bring an umbrella. Don’t let anyone see you with the umbrella in front of a computer screen!
They are called zones (Solaris), workload partitions or WPARs (AIX), “secure resource partitions” or SRPs (HP-UX), jails (FreeBSD), or containers (Linux).
Windows chose the Linux rather than the FreeBSD jargon for this technology.
Let’s go for a walk to the harbour.
Containers
In theory, and explained in a way that the writer of this blog can understand, the purpose of a container (or a jail etc.) is to contain (or imprison) all components of an application. This is a very unixy thing.
In DEC-style operating systems (like OpenVMS and, indeed, Windows) the usual idea was that all parts of an application go into one directory:
- DKA0:[APPLICATIONWHATEVER]
This directory then contains files like
- DKA0:[APPLICATIONWHATEVER]WHATEVER.EXE
- DKA0:[APPLICATIONWHATEVER]WHATEVER.DAT
ACLs on individual files settle the issue what should be writable and by whom (configuration files), what should never be writable (program images), and what is user-writable (settings files of various types).
In Windows this has changed a bit and there are now places for program images and places for data and settings files:
- C:\Program Files\Application Whatever
- C:\ProgramData\Application Whatever
In Unix-like systems instead, files are not grouped by application, but by file type:
- /bin (program images, some of them)
- /usr/bin (program images, some others)
- /etc (global configuration files)
- /lib (libraries, some of them)
- /usr/lib (libraries, others)
- /opt (room for more bin and lib directories)
The result is that a single application is likely distributed through five or six directories or more.
A container solves this problem (and others) by isolating all those files in a single place and then making the application believe the files are in their proper locations indeed.
Smart people would say things like “containers virtualise namespace” and that means that an application running “in” a container does not (necessarily) see the same /whatever or C:\whatever as an application running “outside” that container. This is called “operating system-level virtualisation” and looks to me similar to session virtualistion, which pretends that a SSH deamon is a VT100 terminal that can safely be talked to.
Windows Containers
To enable Windows containers you have to enable Windows containers:
PS C:\> Add-WindowsFeature Containers
Success Restart Needed Exit Code Feature Result
------- -------------- --------- --------------
True Yes SuccessRest... {Containers}
WARNING: You must restart this server to finish the installation process.
PS C:\>
Docker
One program that created, configured, and starts (and stops) containers is Docker. It is available for Linux and Windows. (It is also available for Mac OS X but on Mac OS X it only controls Linux containers in a Linux VM not Mac OS X containers.)
You can find it here: Download Docker
Pick a reasonable recent version. I seem to have ended up with 29.3.1.
In the archive you should find two files, docker.exe and dockerd.exe. The first is the Docker client, the second is the Docker server (deamon). The Docker client connects to the Docker server (on the same machine) and tells it to create, modify, start, and stop Windows containers. It also creates and configured container images. Copy both files into the C:\Windows\System32 directory. (You can copy them elsewhere, but let’s do it the Unix way for now.)
While you can simply run dockerd.exe, you should probably configure dockerd as a Windows service:
PS C:\WINDOWS\system32> .\dockerd.exe --register-service
PS C:\WINDOWS\system32> Get-Service docker*
Status Name DisplayName
------ ---- -----------
Stopped docker Docker Engine
PS C:\WINDOWS\system32>This service runs as LocalSystem because it actually is a system service.
All relevant data files appear to be stored in C:\ProgramData\docker.
You can create a group Docker-Users, tell Docker about it, and add accounts to it that will then have full permissions to create and modify containers. Note that this is essentially giving those accounts full administrator privileges because they can build images or run containers that allow escaping to the host system.
Creating such a group and adding user benoit to it:
PS C:\ProgramData\docker> New-LocalGroup Docker-Users
Name Description
---- -----------
Docker-Users
PS C:\ProgramData\docker> Add-LocalGroupMember Docker-Users benoit
PS C:\ProgramData\docker> md config
Directory: C:\ProgramData\docker
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 4/25/2026 8:19 PM config
PS C:\ProgramData\docker> '{ "group": "docker-users" }'|Out-File .\config\daemon.json
PS C:\ProgramData\docker> Restart-Service Docker
PS C:\ProgramData\docker>I recommend using this group anyway to avoid embarassing mistakes when confusing being inside a container and being outside a container. (This falls into the “type hostname before you type shutdown” category of computer safety.) It also potentially limits the effect of malware that travels with strange container images.
Container Images
A “container image” is to a container what a “program image” is to a program: it’s a file (or rather a bunch of files) that represents the running application unstarted on disk.
Container images are created or downloaded or modified based on a downloaded or created base image. Base images typically represent plain operating systems or operating systems with some software pre-installed.
You can find base images on Docker Hub, specifically on Docker Hub Microsoft for Windows.
The interesting base images are Windows Server Core image and Windows Nano Server image. Each page is good enough to give us the command needed to “pull” (download) the image. You need to pull the image matching your operating system version exactly!
PS C:\> docker pull mcr.microsoft.com/windows/servercore:ltsc2025
ltsc2025: Pulling from windows/servercore
97e96e9cbeb1: Downloading [> ] 10.8MB/1.482GB
f82b995f31e7: Downloading [===> ] 39.99MB/593MBIt takes a while to pull a base image.
PS C:\> docker pull mcr.microsoft.com/windows/servercore:ltsc2025
ltsc2025: Pulling from windows/servercore
97e96e9cbeb1: Pull complete
f82b995f31e7: Pull complete
Digest: sha256:83374b6927f7945bb0933d03f158f84b03182017e2694fa23aedd24ea51434e4
Status: Downloaded newer image for mcr.microsoft.com/windows/servercore:ltsc2025
mcr.microsoft.com/windows/servercore:ltsc2025
PS C:\> docker pull mcr.microsoft.com/windows/nanoserver:ltsc2025
ltsc2025: Pulling from windows/nanoserver
d7986950dcd8: Downloading [============================> ] 109.2MB/193.9MBYou can see that the Nano Server image is much smaller than the Server Core image.
PS C:\> docker pull mcr.microsoft.com/windows/servercore:ltsc2025
ltsc2025: Pulling from windows/servercore
97e96e9cbeb1: Pull complete
f82b995f31e7: Pull complete
Digest: sha256:83374b6927f7945bb0933d03f158f84b03182017e2694fa23aedd24ea51434e4
Status: Downloaded newer image for mcr.microsoft.com/windows/servercore:ltsc2025
mcr.microsoft.com/windows/servercore:ltsc2025
PS C:\>
PS C:\> docker pull mcr.microsoft.com/windows/nanoserver:ltsc2025
ltsc2025: Pulling from windows/nanoserver
d7986950dcd8: Pull complete
Digest: sha256:af9cf5183e68ff0beee87a795e5761ef9143fc6ee7e3d785b5920eda6f5e03fb
Status: Downloaded newer image for mcr.microsoft.com/windows/nanoserver:ltsc2025
mcr.microsoft.com/windows/nanoserver:ltsc2025
PS C:\>And we have two base images.
Docker Commands
- docker pull downloads an image
- docker images shows installed images
PS C:\> docker images
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
mcr.microsoft.com/windows/nanoserver:ltsc2025 299c3c7db826 487MB 0B
mcr.microsoft.com/windows/servercore:ltsc2025 a5c9d9f8dc6e 4.94GB 0B
PS C:\>- docker tag gives an image a nicer name
PS C:\> docker tag mcr.microsoft.com/windows/nanoserver:ltsc2025 nanoserver
PS C:\> docker tag mcr.microsoft.com/windows/servercore:ltsc2025 servercore
PS C:\> docker images
i Info → U In Use
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
mcr.microsoft.com/windows/nanoserver:ltsc2025 299c3c7db826 487MB 0B
mcr.microsoft.com/windows/servercore:ltsc2025 a5c9d9f8dc6e 4.94GB 0B
nanoserver:latest 299c3c7db826 487MB 0B
servercore:latest a5c9d9f8dc6e 4.94GB 0B
PS C:\>- docker run starts a container based on an image
- docker run -d starts a container as a detached process
- docker run -i starts a container with ability to be used interactively
- docker run -t starts a container with a terminal attached to it
- docker run -dit starts a container as a detached process with a terminal attached to it and the ability to be used interactively
- docker ps shows running containers
- docker exec -it executes a command inside a container interactively via a terminal
- docker stop stops a running container
- docker ps -a shows all containers, running and not
PS C:\Users\benoit> docker run -dit servercore cmd
b8ac6cb6c16924405f90b371b430b675aeaf5ef4284e9d1505b185cf88e31a59
PS C:\Users\benoit> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b8ac6cb6c169 servercore "cmd" 11 seconds ago Up 8 seconds elegant_leavitt
PS C:\Users\benoit> docker exec -it elegant_leavitt hostname
b8ac6cb6c169
PS C:\Users\benoit> hostname
Champignac
PS C:\Users\benoit> docker stop elegant_leavitt
elegant_leavitt
PS C:\Users\benoit> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
PS C:\Users\benoit> docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b8ac6cb6c169 servercore "cmd" About a minute ago Exited (3221226219) 11 seconds ago elegant_leavitt
PS C:\Users\benoit>You can see that the running container has a different hostname that the host.
The docker run command above starts a container for running cmd using the servercore image.
- docker start starts an existing container (visible with ps -a)
Note what happens when starting a container.
- Processes running inside the container (here cmd) are also visible outside the container.
- Processes running inside the container run in a different session than processes running outside the container (here session 3 while the calling user is in session 2).
- Processes running outside the container (here winver) are not visible inside the container.
- The account inside the countainer is user manager\containeradministrator.
- That account is a member of the (container’s) Administrators group.
PS C:\Users\benoit> docker start elegant_leavitt
elegant_leavitt
PS C:\Users\benoit> Get-Process cmd
Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName
------- ------ ----- ----- ------ -- -- -----------
76 7 5136 5096 4020 3 cmd
PS C:\Users\benoit> query session
SESSIONNAME USERNAME ID STATE TYPE DEVICE
services 0 Disc
>console benoit 2 Active
rdp-tcp 65536 Listen
PS C:\Users\benoit> docker exec -it elegant_leavitt powershell Get-Process cmd
Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName
------- ------ ----- ----- ------ -- -- -----------
76 6 4108 5084 0.02 4020 3 cmd
PS C:\Users\benoit> winver
PS C:\Users\benoit> Get-Process winver
Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName
------- ------ ----- ----- ------ -- -- -----------
184 14 2336 16500 0.08 3928 2 winver
PS C:\Users\benoit> docker exec -it elegant_leavitt powershell Get-Process winver
Get-Process : Cannot find a process with the name "winver". Verify the process name and call the cmdlet again.
At line:1 char:1
+ Get-Process winver
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (winver:String) [Get-Process], ProcessCommandException
+ FullyQualifiedErrorId : NoProcessFoundForGivenName,Microsoft.PowerShell.Commands.GetProcessCommand
PS C:\Users\benoit> docker exec -it elegant_leavitt whoami
user manager\containeradministrator
PS C:\Users\benoit> docker exec -it elegant_leavitt whoami /groups
GROUP INFORMATION
-----------------
Group Name Type SID Attributes
==================================== ================ ============ ===============================================================
Mandatory Label\High Mandatory Level Label S-1-16-12288
Everyone Well-known group S-1-1-0 Mandatory group, Enabled by default, Enabled group
BUILTIN\Users Alias S-1-5-32-545 Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\SERVICE Well-known group S-1-5-6 Mandatory group, Enabled by default, Enabled group
CONSOLE LOGON Well-known group S-1-2-1 Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\Authenticated Users Well-known group S-1-5-11 Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\This Organization Well-known group S-1-5-15 Mandatory group, Enabled by default, Enabled group
LOCAL Well-known group S-1-2-0 Mandatory group, Enabled by default, Enabled group
BUILTIN\Administrators Alias S-1-5-32-544 Mandatory group, Enabled by default, Enabled group, Group owner
Unknown SID type S-1-5-93-0 Mandatory group, Enabled by default, Enabled group
PS C:\Users\benoit>And what’s even worse, processes running inside the container do not have associated account names:
PS C:\> Get-Process cmd -IncludeUserName
Handles WS(K) CPU(s) Id UserName ProcessName
------- ----- ------ -- -------- -----------
76 5172 0.02 4020 cmd
76 5084 0.02 4200 cmd
PS C:\- docker run -p runs a container with a port mapping, host to container
- docker run -v runs a container a directory mapping, host to container
Imagine -v like this:
PS C:\> md foo
Directory: C:\
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 4/25/2026 11:49 PM foo
PS C:\> ni .\foo\bar
Directory: C:\foo
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 4/25/2026 11:49 PM 0 bar
PS C:\> docker run -dit -v "C:\foo:C:\foofromhost" servercore cmd
9eac0388e2ca4cb6aaa4560a14d47c1696b41e35d9069210860daaf6751f3530
PS C:\> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9eac0388e2ca servercore "cmd" 9 seconds ago Up 6 seconds adoring_allen
PS C:\> docker exec -it adoring_allen cmd /c dir c:\
Volume in drive C has no label.
Volume Serial Number is 1001-F4AE
Directory of c:\
04/25/2026 11:49 PM <DIR> foofromhost
01/11/2026 11:44 AM <DIR> inetpub
01/11/2026 11:35 AM 5,647 License.txt
04/13/2026 09:05 AM <DIR> Program Files
04/13/2026 08:59 AM <DIR> Program Files (x86)
04/13/2026 09:05 AM <DIR> Users
04/25/2026 11:50 PM <DIR> Windows
1 File(s) 5,647 bytes
6 Dir(s) 136,177,917,952 bytes free
PS C:\> docker exec -it adoring_allen cmd /c dir c:\foofromhost
Volume in drive C has no label.
Volume Serial Number is 1001-F4AE
Directory of c:\foofromhost
04/25/2026 11:49 PM <DIR> .
04/25/2026 11:49 PM 0 bar
1 File(s) 0 bytes
1 Dir(s) 28,357,349,376 bytes free
PS C:\>I will go into details on port forwarding in a future article on running Internet Information Server in a container.
- docker commit “commits” a container into a new image
PS C:\> docker commit adoring_allen newimage
Error response from daemon: windows does not support commit of a running container
PS C:\> docker stop adoring_allen
adoring_allen
PS C:\> docker commit adoring_allen newimage
sha256:15e6323e548c0044e70dddf1094594c1f72f2ba4a33855f8aa6f15d4bd5ec9cb
PS C:\> docker images
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
mcr.microsoft.com/windows/nanoserver:ltsc2025 299c3c7db826 487MB 0B
mcr.microsoft.com/windows/servercore:ltsc2025 a5c9d9f8dc6e 4.94GB 0B U
nanoserver:latest 299c3c7db826 487MB 0B
newimage:latest 15e6323e548c 5.01GB 0B
servercore:latest a5c9d9f8dc6e 4.94GB 0B U
PS C:\>The idea is that you take a base image, run a container based on it, do whatever you want with it, and then create a new image from the resulting state.
- docker rm removes a (non-running) container
- docker rmi removes an image
PS C:\Users\benoit> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
PS C:\Users\benoit> docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9eac0388e2ca servercore "cmd" 7 minutes ago Exited (3221226219) 4 minutes ago adoring_allen
PS C:\Users\benoit> docker rm adoring_allen
adoring_allen
PS C:\Users\benoit> docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
PS C:\Users\benoit> docker images
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
mcr.microsoft.com/windows/nanoserver:ltsc2025 299c3c7db826 487MB 0B
mcr.microsoft.com/windows/servercore:ltsc2025 a5c9d9f8dc6e 4.94GB 0B
nanoserver:latest 299c3c7db826 487MB 0B
newimage:latest 15e6323e548c 5.01GB 0B
servercore:latest a5c9d9f8dc6e 4.94GB 0B
PS C:\Users\benoit> docker rmi newimage
Untagged: newimage:latest
Deleted: sha256:15e6323e548c0044e70dddf1094594c1f72f2ba4a33855f8aa6f15d4bd5ec9cb
Deleted: sha256:4d419e6b12c4947b1a0ed5465f19d031fc0acec3913ea8cf8314aa2a905511c8
PS C:\Users\benoit> docker images
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
mcr.microsoft.com/windows/nanoserver:ltsc2025 299c3c7db826 487MB 0B
mcr.microsoft.com/windows/servercore:ltsc2025 a5c9d9f8dc6e 4.94GB 0B
nanoserver:latest 299c3c7db826 487MB 0B
servercore:latest a5c9d9f8dc6e 4.94GB 0B
PS C:\Users\benoit>
Differences between Server Core and Nano Server Images
Running a quick dir in both variants shows a huge difference quickly:
S C:\Users\benoit> docker run -dit servercore cmd
64e832fe43e5946c3efcf06ccd5ff81821b2336df517803b6d975fd99a6542a3
PS C:\Users\benoit> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
64e832fe43e5 servercore "cmd" 6 seconds ago Up 4 seconds eager_gagarin
PS C:\Users\benoit> docker exec -it eager_gagarin cmd /c dir c:\
Volume in drive C has no label.
Volume Serial Number is 1001-F4AE
Directory of c:\
01/11/2026 11:44 AM <DIR> inetpub
01/11/2026 11:35 AM 5,647 License.txt
04/13/2026 09:05 AM <DIR> Program Files
04/13/2026 08:59 AM <DIR> Program Files (x86)
04/13/2026 09:05 AM <DIR> Users
04/25/2026 11:59 PM <DIR> Windows
1 File(s) 5,647 bytes
5 Dir(s) 136,177,942,528 bytes free
PS C:\Users\benoit> docker run -dit nanoserver cmd
98399bd440816470a335d9b9dc6e6d7e6ff252c0d7a9d616b4a0297ac352bd0b
PS C:\Users\benoit> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
98399bd44081 nanoserver "cmd" 5 seconds ago Up 2 seconds brave_fermat
64e832fe43e5 servercore "cmd" 43 seconds ago Up 41 seconds eager_gagarin
PS C:\Users\benoit> docker exec -it brave_fermat cmd /c dir c:\
Volume in drive C has no label.
Volume Serial Number is 76D0-1E23
Directory of c:\
04/13/2026 08:36 AM 5,647 License.txt
04/13/2026 08:39 AM <DIR> Users
04/25/2026 11:59 PM <DIR> Windows
1 File(s) 5,647 bytes
2 Dir(s) 136,183,414,784 bytes free
PS C:\Users\benoit>| Server Core | Nano Server |
| PowerShell | - |
| WoW (32 bit emulation) | - |
| .NET and .NET Framework | .NET only |
| Can run stand-alone | Container only |
| Meant for complete Windows Server applications | Suitable only for applications developed for the Nano Server container |
| 5 GB image size | 500 MB image size |
| No GUI applications, no RDP | No GUI applications, no RDP |
Exporting and Importing Containers
- docker save saves an image into a tarball (commit the container to an image to save)
- docker load loads an image from a tarball
PS C:\Users\benoit> docker commit brave_fermat brave_fermat
sha256:18972e6d20078a7fa3bfb701e451153c833f4fef588bec8f639a7cb64c85f672
PS C:\Users\benoit> docker images
i Info → U In Use
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
brave_fermat:latest 18972e6d2007 489MB 0B
mcr.microsoft.com/windows/nanoserver:ltsc2025 299c3c7db826 487MB 0B U
mcr.microsoft.com/windows/servercore:ltsc2025 a5c9d9f8dc6e 4.94GB 0B U
nanoserver:latest 299c3c7db826 487MB 0B U
servercore:latest a5c9d9f8dc6e 4.94GB 0B U
PS C:\Users\benoit> docker save brave_fermat -o brave_fermat.tar
PS C:\Users\benoit> docker rmi brave_fermat
Untagged: brave_fermat:latest
Deleted: sha256:18972e6d20078a7fa3bfb701e451153c833f4fef588bec8f639a7cb64c85f672
Deleted: sha256:653f586307eb57d859a8f172075d849734123db34291f5ada26506ef7b0377d8
PS C:\Users\benoit> docker load -i brave_fermat.tar
cb6a8faeb776: Loading layer [==================================================>] 2.018MB/2.018MB
Loaded image: brave_fermat:latest
PS C:\Users\benoit>Next: TBD