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/593MB

It 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.9MB

You 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.

  1. Processes running inside the container (here cmd) are also visible outside the container.
  2. 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).
  3. Processes running outside the container (here winver) are not visible inside the container.
  4. The account inside the countainer is user manager\containeradministrator.
  5. 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