The tricky part of using runhcs has been getting the layers correct. While I haven’t had any luck, I have managed to created Windows containers using ctr
and containerd
.
Installing containerd
is a manual process on Windows. These steps give general guidance on what is needed: enable the containers
feature in Windows, download the tar file from GitHub, extract it, add it to the path, generate a default configuration file, register the service, and start it.
Enable-WindowsOptionalFeature -Online -FeatureName containers -All
mkdir "c:\Program Files\containerd"
curl.exe -L https://github.com/containerd/containerd/releases/download/v2.2.1/containerd-2.2.1-windows-amd64.tar.gz -o containerd-windows-amd64.tar.gz
tar.exe xvf .\containerd-windows-amd64.tar.gz -C "c:\Program Files\containerd"
$Path = [Environment]::GetEnvironmentVariable("PATH", "Machine") + [IO.Path]::PathSeparator + "$Env:ProgramFiles\containerd\bin"
Environment]::SetEnvironmentVariable( "Path", $Path, "Machine")
containerd.exe config default | Out-File "c:\Program Files\containerd\config.toml" -Encoding ascii
containerd --register-service
net start containerd
With that out of the way, pull nanoserver:ltsc2022
from Microsoft’s container registry.
c:\> ctr image pull mcr.microsoft.com/windows/nanoserver:ltsc2022
List which snapshots are available: nanoserver
has one, but servercore
has two.
c:\> ctr snapshot ls
KEY PARENT KIND
sha256:44b913d145adda5364b5465664644b11282ed3c4b9bd9739aa17832ee4b2b355 Committed
Take a snapshot of nanoserver
, which creates a writeable scratch layer. --mounts
is key here. Without it, you won’t know where the layers are. They are held below C:\ProgramData\containerd\root\io.containerd.snapshotter.v1.windows\snapshots
in numbered folders. The mapping between numbers and keys is stored in metadata.db
in BoltDB format. With the --mounts
command line option, we see the source
path and list of paths in parentLayerPaths
.
c:\> ctr snapshots prepare --mounts my-test sha256:44b913d145adda5364b5465664644b11282ed3c4b9bd9739aa17832ee4b2b355
[
{
"Type": "windows-layer",
"Source": "C:\\ProgramData\\containerd\\root\\io.containerd.snapshotter.v1.windows\\snapshots\\21",
"Target": "",
"Options": [
"rw",
"parentLayerPaths=[\"C:\\\\ProgramData\\\\containerd\\\\root\\\\io.containerd.snapshotter.v1.windows\\\\snapshots\\\\20\"]"
]
}
]
As you can see from ctr snapshot ls
and ctr snapshot info
, the layer paths aren’t readily available. This discussion is a sample of the creative approaches to getting the paths!
c:\> ctr snapshot ls
KEY PARENT KIND
my-test sha256:44b913d145adda5364b5465664644b11282ed3c4b9bd9739aa17832ee4b2b355 Active
sha256:44b913d145adda5364b5465664644b11282ed3c4b9bd9739aa17832ee4b2b355 Committed
c:\> ctr snapshot info my-test
{
"Kind": "Active",
"Name": "my-test",
"Parent": "sha256:44b913d145adda5364b5465664644b11282ed3c4b9bd9739aa17832ee4b2b355",
"Labels": {
"containerd.io/gc.root": "2025-06-11T12:28:43Z"
},
"Created": "2025-06-11T16:33:43.144011Z",
"Updated": "2025-06-11T16:33:43.144011Z"
}
Here’s the directory listing for reference.
c:\> dir C:\ProgramData\containerd\root\io.containerd.snapshotter.v1.windows\snapshots
Volume in drive C has no label.
Volume Serial Number is F0E9-1E81
Directory of C:\ProgramData\containerd\root\io.containerd.snapshotter.v1.windows\snapshots
11/06/2025 16:33 <DIR> .
11/06/2025 08:19 <DIR> ..
11/06/2025 08:31 <DIR> 2
11/06/2025 16:32 <DIR> 20
11/06/2025 16:33 <DIR> 21
11/06/2025 08:20 <DIR> rm-1
11/06/2025 08:20 <DIR> rm-2
11/06/2025 08:22 <DIR> rm-3
Now we need to prepare a config.json
file. The layerFolders
structure can be populated with the information from above. The order is important; preserve the order from parentLayerPaths
, then append the scratch layer. It looks obvious when there are just two layers, but for servercore:ltsc2022
where there are two parent layers, the order looks curious as the parent layers are given in reverse order and the scratch layer is last, e.g. 24, 23, 25
where 23 and 24 are the parents and 25 is the snapshot.
{
"ociVersion": "1.1.0",
"process": {
"user": {
"uid": 0,
"gid": 0,
"username": "ContainerUser"
},
"args": [
"cmd",
"/c",
"echo test"
],
"cwd": ""
},
"root": {
"path": ""
},
"windows": {
"layerFolders": [
"C:\\ProgramData\\containerd\\root\\io.containerd.snapshotter.v1.windows\\snapshots\\20",
"C:\\ProgramData\\containerd\\root\\io.containerd.snapshotter.v1.windows\\snapshots\\21"
],
"ignoreFlushesDuringBoot": true,
"network": {
"allowUnqualifiedDNSQuery": true
}
}
}
We can now run the container.
c:\> ctr run --rm --config .\config.json my-container