Windows контейнеры вручную. Часть 1: Ставим containerd без Docker и Kubernetes

Все части серии «Windows контейнеры вручную»

В серии Kubernetes для самых маленьких Windows-контейнеры появлялись как часть кластера: часть 36 объясняла, из чего они состоят, часть 37 — как устроена их сеть, а в части 38 containerd ставил готовый скрипт, и дальше контейнерами управлял kubelet. В этой серии мы пойдём в обратную сторону — вниз, убирая по одному слою инструментов, пока не останется контейнер, созданный прямыми вызовами Windows.

Сегодня первый шаг: Windows Server без Docker и без Kubernetes, на который мы руками ставим containerd, сеть для контейнеров и nerdctl. Запустим первый контейнер, дадим ему сеть, опубликуем порт, подключим к нему папки и файлы хоста, ограничим память и процессор и разберёмся, какие бывают уровни изоляции. Всё — с изоляцией процессов: это основной режим Windows-контейнеров на сервере, тот же, в котором работают поды в Kubernetes.

Что такое server silo, объекты задания и чем изоляция процессов отличается от изоляции Hyper-V, подробно разобрано в части 36, а HNS — служба, которая строит сеть контейнеров, — в части 37. Здесь эти понятия используются без повторного объяснения. Скрипт, который делает всё описанное ниже, кроме установки компонента Containers, и файлы конфигурации лежат в репозитории windows-containers-by-hand.


Стенд

Что Значение
Машина wincont-lab.example.com, 192.168.10.30, виртуальная машина Hyper-V: 4 vCPU, 8 ГБ, диск 64 ГБ
ОС Windows Server 2025 Datacenter, Server Core, сборка 10.0.26100.33650
containerd 2.4.2
nerdctl 2.4.2
CNI-плагины Microsoft windows-container-networking 0.3.3
Образы mcr.microsoft.com/windows/nanoserver:ltsc2025, servercore:ltsc2025, пример ASP.NET mcr.microsoft.com/dotnet/samples:aspnetapp-nanoserver-ltsc2025

Машина свежая: установлены только обновления, Docker и Kubernetes на ней никогда не было. Все команды выполняются в PowerShell от имени администратора — локально или через Enter-PSSession.

Вот что мы соберём к концу части:

   nerdctl            ctr ──────► CNI-плагин nat.exe ──► HNS: сеть nat, 172.30.0.0/16
      │                │
      └───────┬────────┘
              ▼  \\.\pipe\containerd-containerd
   ┌─ containerd (служба Windows) ───────┐
   │ образы и слои: снапшоттер windows   │
   └──────────┬──────────────────────────┘
              ▼
   containerd-shim-runhcs-v1.exe (по shim на контейнер)
              │
              ▼
   HCS (служба vmcompute) → server silo контейнера

Сверху — два клиента: ctr, который идёт вместе с containerd, и nerdctl с привычными по Docker командами. containerd хранит образы и распаковывает слои, а сам контейнер создаёт shim из проекта hcsshim, обращаясь к HCS — службе Windows, которая и создаёт server silo. Сеть containerd здесь не настраивает: CNI-плагин запускает клиент, ctr или nerdctl. Сам containerd делает это только для kubelet.


Компонент Containers

До того как что-то ставить, посмотрим, чего в системе нет:

Get-WindowsFeature Containers, Hyper-V | Format-Table Name, DisplayName, InstallState
Get-Service vmcompute, hns -ErrorAction SilentlyContinue
Get-ChildItem C:\Windows\System32\computecore.dll, C:\Windows\System32\vmcompute.exe, C:\Windows\System32\hcsdiag.exe -ErrorAction SilentlyContinue | Format-Table Name, Length
Name       DisplayName InstallState
----       ----------- ------------
Hyper-V    Hyper-V        Available
Containers Containers     Available

Name             Length
----             ------
computecore.dll 1031520

Служб vmcompute и hns нет, нет и утилит. Есть только computecore.dll — клиентская библиотека HCS API: через неё к HCS обращаются все программы, которые создают контейнеры и виртуальные машины, и к ней мы вернёмся в пятой части. Сам HCS и HNS приходят с компонентом Containers:

Install-WindowsFeature -Name Containers
Restart-Computer
WARNING: You must restart this server to finish the installation process.

Success Restart Needed Exit Code      Feature Result
------- -------------- ---------      --------------
True    Yes            SuccessRest... {Containers}

Компонент Hyper-V для изоляции процессов не нужен: контейнеры работают прямо на ядре хоста, без виртуальной машины. После перезагрузки:

Get-Service vmcompute, hns | Format-Table Name, DisplayName, Status, StartType
Get-Module -ListAvailable HostNetworkingService | Format-Table Name, Version
Name      DisplayName                   Status StartType
----      -----------                   ------ ---------
hns       Host Network Service         Running    Manual
vmcompute Hyper-V Host Compute Service Stopped    Manual

Name                  Version
----                  -------
HostNetworkingService 1.0.0.1

vmcompute — это и есть HCS (Host Compute Service), он стартует при первом обращении. Ещё появились vmcompute.exe, hcsdiag.exe и hnsdiag.exe в System32 и модуль HostNetworkingService. У модуля есть особенность, которая встретится нам дальше: в нём только команды Get-* и Remove-* — посмотреть сети и конечные точки HNS можно, а создать их этим модулем нельзя.


containerd

Скачиваем и проверяем

containerd выпускает сборки для Windows на GitHub вместе с файлами контрольных сумм:

$v = '2.4.2'
$base = "https://github.com/containerd/containerd/releases/download/v$v"
Set-Location $env:TEMP
curl.exe -sSLO "$base/containerd-$v-windows-amd64.tar.gz"
curl.exe -sSLO "$base/containerd-$v-windows-amd64.tar.gz.sha256sum"
Get-Content "containerd-$v-windows-amd64.tar.gz.sha256sum"
(Get-FileHash "containerd-$v-windows-amd64.tar.gz" -Algorithm SHA256).Hash.ToLower()
tar.exe -tzf "containerd-$v-windows-amd64.tar.gz"
cae2a5caf41b32ebb9b741461c163e36ebfa1a9afea8d47c66b66a27b1eb2303  containerd-2.4.2-windows-amd64.tar.gz
cae2a5caf41b32ebb9b741461c163e36ebfa1a9afea8d47c66b66a27b1eb2303
bin/
bin/ctr.exe
bin/containerd-shim-runhcs-v1.exe
bin/containerd-stress.exe
bin/containerd.exe

curl.exe и tar.exe входят в Windows, ставить их не нужно. Суммы совпали. В архиве четыре программы:

  • containerd.exe — сам демон;
  • ctr.exe — его клиент для отладки, умеющий всё, что умеет API containerd, без удобств;
  • containerd-shim-runhcs-v1.exe — shim для Windows: containerd запускает по экземпляру на каждый контейнер, и именно shim обращается к HCS. На Linux на этом месте стоит containerd-shim-runc-v2 с runc;
  • containerd-stress.exe — нагрузочный тест, нам не понадобится.

Распаковываем в C:\Program Files\containerd и добавляем bin в системный PATH:

$dir = "$env:ProgramFiles\containerd"
New-Item -ItemType Directory $dir -Force | Out-Null
tar.exe -xzf "$env:TEMP\containerd-$v-windows-amd64.tar.gz" -C $dir
$p = [Environment]::GetEnvironmentVariable('Path', 'Machine')
[Environment]::SetEnvironmentVariable('Path', "$p;$dir\bin", 'Machine')
$env:Path += ";$dir\bin"
containerd --version
containerd github.com/containerd/containerd/v2 v2.4.2 b7a25261e029d68978bbf290df0a62fc819b3c51

Конфигурация

Без файла конфигурации containerd работает со значениями по умолчанию, но удобнее, когда они лежат перед глазами. containerd на Windows ищет его в C:\Program Files\containerd\config.toml:

containerd config default | Set-Content "$dir\config.toml" -Encoding ascii
Select-String -Path "$dir\config.toml" -Pattern "^root|^state|snapshotter = |default_runtime_name|bin_dirs|conf_dir|platforms|address = '\\" | ForEach-Object Line
root = 'C:\ProgramData\containerd\root'
state = 'C:\ProgramData\containerd\state'
    snapshotter = 'windows'
      default_runtime_name = 'runhcs-wcow-process'
      bin_dirs = ['C:\Program Files\containerd\cni\bin']
      conf_dir = 'C:\Program Files\containerd\cni\conf'
    platforms = ['windows(10.0.26100)/amd64', 'linux/amd64']
    address = '\\.\pipe\containerd-containerd'
  • root и state — где containerd хранит образы, слои и метаданные (root) и то, что живёт только пока работает служба (state). Что лежит в root, посмотрим в третьей части.
  • snapshotter = 'windows' — способ превращать слои образа в файловую систему контейнера. Снапшоттер windows раскладывает слои в папки, а записываемый слой контейнера делает отдельным виртуальным диском.
  • default_runtime_name = 'runhcs-wcow-process' — среда выполнения по умолчанию: runhcs, Windows-контейнер на Windows (WCOW), изоляция процессов. Рядом в файле описана runhcs-wcow-hypervisor — та же среда с изоляцией Hyper-V. Эти две строчки — секция CRI, то есть настройки, которыми пользуется kubelet; ctr и nerdctl выбирают изоляцию сами, и по умолчанию тоже process.
  • bin_dirs и conf_dir — где лежат CNI-плагины и их конфигурация. Пока этих папок нет.
  • platforms — какие образы containerd согласится скачивать: Windows сборки 26100 и Linux. Номер сборки в скобках важен: из образа с несколькими вариантами containerd выберет вариант для своей сборки — правила подробно разобраны в части 39.
  • address — именованный канал, через который клиенты разговаривают с containerd. Именно его увидит nerdctl.

Служба

containerd умеет сам зарегистрироваться службой Windows:

containerd --register-service
Start-Service containerd
Get-Service containerd | Format-Table Name, DisplayName, Status, StartType
Get-CimInstance Win32_Service -Filter "Name='containerd'" | Format-List PathName, StartName
ctr version
Name       DisplayName  Status StartType
----       -----------  ------ ---------
containerd containerd  Running Automatic

PathName  : "C:\Program Files\containerd\bin\containerd.exe" --run-service
StartName : LocalSystem

Client:
  Version:  v2.4.2
  Revision: b7a25261e029d68978bbf290df0a62fc819b3c51
  Go version: go1.27.2

Server:
  Version:  v2.4.2
  Revision: b7a25261e029d68978bbf290df0a62fc819b3c51
  UUID: a21bd28f-499d-4e16-8c0f-356a6ec8a297

Служба запускается автоматически, от имени LocalSystem.


Первый контейнер через ctr

ctr image pull mcr.microsoft.com/windows/nanoserver:ltsc2025
ctr image ls
└──index (05d1a7fb562f)                 complete   |++++++++++++++++++++++++++++++++++++++|
   └──manifest (6020b6edd804)           complete   |++++++++++++++++++++++++++++++++++++++|
      ├──config (bd66d3cd2318)          complete   |++++++++++++++++++++++++++++++++++++++|
      └──layer (a7eba7a59d48)           complete   |++++++++++++++++++++++++++++++++++++++|
application/vnd.docker.distribution.manifest.list.v2+json sha256:05d1a7fb562f998ed3bc23b21dbd95aa26b3dd10ae921e830449e2db657f6704
Pulling from OCI Registry (mcr.microsoft.com/windows/nanoserver:ltsc2025)  elapsed: 20.2s  total:  185.2  (9.2 MiB/s)

REF                                           TYPE                                                      DIGEST                                                                  SIZE      PLATFORMS     LABELS
mcr.microsoft.com/windows/nanoserver:ltsc2025 application/vnd.docker.distribution.manifest.list.v2+json sha256:05d1a7fb562f998ed3bc23b21dbd95aa26b3dd10ae921e830449e2db657f6704 185.2 MiB windows/amd64 -

ctr показывает, из чего состоит образ: индекс со списком вариантов, из него — манифест для нашей платформы, а в манифесте — конфигурация и единственный слой Nano Server, 185 MiB в сжатом виде. Запускаем:

ctr run --rm mcr.microsoft.com/windows/nanoserver:ltsc2025 hello cmd /c "ver & hostname & echo %USERNAME% & ipconfig"
cmd /c ver
Microsoft Windows [Version 10.0.26100.33674]
minwinpc
ContainerUser

Windows IP Configuration

Microsoft Windows [Version 10.0.26100.33650]

Контейнер запустился и завершился меньше чем за две секунды. Последняя строка — версия самого хоста, и ревизии различаются: 33674 в образе и 33650 на хосте. Для изоляции процессов совпадать должна сборка (26100), а ревизии могут быть разными — часть 36 разбирает, почему. У контейнера своё имя (minwinpc) и свой пользователь по умолчанию (ContainerUser, у Nano Server он не администратор), а ipconfig пуст: ctr сам сеть не настраивает, и у контейнера нет ни одного адаптера. Сеть ему дадим через CNI.


Сеть: CNI и плагин nat

Как контейнер получает сеть

CNI (Container Network Interface) — общая для Linux и Windows спецификация того, как среда выполнения просит сеть для контейнера. Плагин — это обычная программа. Среда выполнения запускает её, передаёт в переменных окружения команду (ADD — подключить контейнер, DEL — отключить, CHECK — проверить), идентификатор контейнера и его сетевое пространство имён, а на стандартный вход — JSON с конфигурацией сети. Плагин делает свою работу и печатает JSON с выданными адресами. Kubernetes пользуется тем же механизмом — так Calico из части 37 подключает поды.

Microsoft выпускает три плагина для Windows: nat, sdnbridge и sdnoverlay. Последние два — для кластеров, где адреса контейнеров видны с других хостов: мост в сеть хоста и наложенная сеть между хостами. Для одной машины нужен nat: контейнеры получают адреса из внутренней подсети, а наружу выходят через трансляцию адресов (NAT) — хост подменяет внутренний адрес контейнера своим и возвращает ответы обратно. Так же работает домашний роутер.

$v = '0.3.3'
$zip = "windows-container-networking-cni-amd64-v$v.zip"
curl.exe -sSLO "https://github.com/microsoft/windows-container-networking/releases/download/v$v/$zip"
curl.exe -sSLO "https://github.com/microsoft/windows-container-networking/releases/download/v$v/$zip.sha256"
Get-Content "$zip.sha256"
(Get-FileHash $zip).Hash.ToLower()
$cni = "$env:ProgramFiles\containerd\cni"
New-Item -ItemType Directory "$cni\bin", "$cni\conf" -Force | Out-Null
tar.exe -xf $zip -C "$cni\bin"
Get-ChildItem "$cni\bin" | Format-Table Name, Length
12ce767c9bc5b8088021339bedd119a85d40bc2e18df5c55ca629b1ef4d346c5  ./release/windows-container-networking-cni-amd64-v0.3.3.zip
12ce767c9bc5b8088021339bedd119a85d40bc2e18df5c55ca629b1ef4d346c5

Name             Length
----             ------
nat.exe         5353984
sdnbridge.exe   5353984
sdnoverlay.exe  5353984

Конфигурация сети nat

В conf кладём описание сети — файл 0-containerd-nat.conflist:

{
    "cniVersion": "0.3.0",
    "name": "nat",
    "plugins": [
        {
            "type": "nat",
            "master": "Ethernet",
            "ipam": {
                "subnet": "172.30.0.0/16",
                "routes": [
                    { "gateway": "172.30.0.1" }
                ]
            },
            "capabilities": {
                "portMappings": true,
                "dns": true
            }
        }
    ]
}
  • name — имя сети. Плагин nat найдёт или создаст сеть HNS с этим именем.
  • plugins — цепочка плагинов, которые вызываются по очереди; у нас один. Поэтому файл называется .conflist. Бывает и старый формат .conf — один плагин без списка, — и ctr его понимает, но nerdctl на нём падает с ошибкой index out of range в verifyNetworkTypes: ему нужен именно список.
  • type — имя исполняемого файла плагина в bin_dirs, то есть nat.exe.
  • master — сетевой адаптер хоста, так он назван в примерах Microsoft. Узнать имя можно командой Get-NetAdapter.
  • ipam — откуда брать адреса: подсеть контейнеров и шлюз, которым станет хост. Подсеть не должна пересекаться ни с сетью хоста, ни с сетями, куда контейнерам нужно ходить.
  • capabilities — что плагин умеет принимать от среды выполнения: публикацию портов и настройки DNS — службы, которая превращает имена вроде mcr.microsoft.com в адреса.
curl.exe -sSL -o "$cni\conf\0-containerd-nat.conflist" https://raw.githubusercontent.com/vitaliy-leschenko/windows-containers-by-hand/main/01-containerd/0-containerd-nat.conflist
Get-HnsNetwork

Get-HnsNetwork ничего не выводит: сетей HNS пока нет. Создавать её руками не придётся — да и встроенным модулем нечем.

Контейнер в сети

ctr run --rm --cni mcr.microsoft.com/windows/nanoserver:ltsc2025 net1 cmd /c "ipconfig & ping -n 2 1.1.1.1"
Windows IP Configuration


Ethernet adapter vEthernet (default-net1_nat):

   Connection-specific DNS Suffix  . : example.com
   Link-local IPv6 Address . . . . . : fe80::8653:cde8:f2a6:5379%17
   IPv4 Address. . . . . . . . . . . : 172.30.150.138
   Subnet Mask . . . . . . . . . . . : 255.255.0.0
   Default Gateway . . . . . . . . . : 172.30.0.1

Pinging 1.1.1.1 with 32 bytes of data:
Reply from 1.1.1.1: bytes=32 time=9ms TTL=54
Reply from 1.1.1.1: bytes=32 time=9ms TTL=54

Ping statistics for 1.1.1.1:
    Packets: Sent = 2, Received = 2, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 9ms, Maximum = 9ms, Average = 9ms

Флаг --cni велит ctr запустить CNI-плагин, и запускает его сам ctr, а не containerd. У контейнера появился адаптер с адресом из нашей подсети и шлюзом 172.30.0.1, а ping — он шлёт эхо-запросы ICMP, служебного протокола IP, на которые отвечает узел, — доходит до внешнего адреса. Адаптер называется default-net1_nat: пространство имён containerd (default), имя контейнера и имя сети. А на хосте теперь есть сеть HNS:

Get-HnsNetwork | Select-Object Name, Type, @{n='AddressPrefix';e={$_.Subnets.AddressPrefix}}, @{n='Gateway';e={$_.Subnets.GatewayAddress}} | Format-List
Name          : nat
Type          : NAT
AddressPrefix : 172.30.0.0/16
Gateway       : 172.30.0.1

Её создал плагин nat при первом вызове ADD, потому что сети с именем nat не было. После --rm ctr вызвал DEL, и плагин удалил конечную точку контейнера (Get-HnsEndpoint снова пуст), а сеть осталась для следующих контейнеров.

В Nano Server нет nslookup, поэтому DNS проверим запросом к реестру образов через curl.exe, который в образе есть:

ctr run --rm --cni mcr.microsoft.com/windows/nanoserver:ltsc2025 net2 cmd /c "ipconfig /all | findstr /i dns & curl.exe -sS -o NUL -w %{http_code} https://mcr.microsoft.com/v2/"
   Primary Dns Suffix  . . . . . . . :
   DNS Suffix Search List. . . . . . : example.com
   Connection-specific DNS Suffix  . : example.com
   DNS Servers . . . . . . . . . . . : 192.168.10.1
   Connection-specific DNS Suffix Search List :
200

DNS-сервер и суффикс контейнер получил те же, что у хоста, имя разрешилось, реестр ответил кодом 200.


nerdctl

ctr годится для проверок, но каждый раз писать --cni и придумывать имя контейнера неудобно. nerdctl — клиент containerd с командами Docker: run, ps, exec, -p, -v. Это один файл:

$v = '2.4.2'
$f = "nerdctl-$v-windows-amd64.tar.gz"
curl.exe -sSLO "https://github.com/containerd/nerdctl/releases/download/v$v/$f"
curl.exe -sSLO "https://github.com/containerd/nerdctl/releases/download/v$v/SHA256SUMS"
(Select-String -Path SHA256SUMS -Pattern $f -SimpleMatch).Line
(Get-FileHash $f).Hash.ToLower()
tar.exe -xzf $f -C "$env:ProgramFiles\containerd\bin"
nerdctl info
nerdctl network ls
03891a599848e819dee0f79f1adb6bd3022df737980df7ea5ffe0649fec9cd80  nerdctl-2.4.2-windows-amd64.tar.gz
03891a599848e819dee0f79f1adb6bd3022df737980df7ea5ffe0649fec9cd80

Client:
 Namespace:     default
 Debug Mode:    false

Server:
 Server Version: v2.4.2
 Storage Driver: windows
 Logging Driver: json-file
 Plugins:
  Log:     fluentd journald json-file none syslog
  Storage: windows-lcow cimfs windows
 Kernel Version:   10.0 26100 (26100.33648.amd64fre.lt_release_svc_prod1.261009-2239)
 Operating System: Microsoft Windows Server Version 24H2 (OS Build 26100.33650)
 OSType:           windows
 Architecture:     x86_64
 CPUs:             4
 Total Memory:     7.999GiB
 Name:             wincont-lab

NETWORK ID    NAME    FILE
              nat     C:\Program Files\containerd\cni\conf\0-containerd-nat.conflist
              host
              none

nerdctl нашёл containerd по каналу \\.\pipe\containerd-containerd и сеть nat в каталоге CNI. Своей конфигурации ему не нужно. При каждом запуске он будет предупреждать, что сеть nat создана не им:

time="2026-11-08T02:24:09+03:00" level=warning msg="default network named \"nat\" does not have an internal nerdctl ID or nerdctl-managed config file, it was most likely NOT created by nerdctl"

Это правда и ничему не мешает. Дальше в выводе это предупреждение опущено.

Публикуем порт

Запустим пример ASP.NET от Microsoft на Nano Server — тот же, что в серии о Kubernetes. Он слушает порт 8080:

nerdctl run -d --name web -p 8080:8080 mcr.microsoft.com/dotnet/samples:aspnetapp-nanoserver-ltsc2025
nerdctl ps
CONTAINER ID    IMAGE                                                             COMMAND                 CREATED                   STATUS    PORTS                     NAMES
7c686c1fbb87    mcr.microsoft.com/dotnet/samples:aspnetapp-nanoserver-ltsc2025    "AspNetCoreRazorApp"    Less than a second ago    Up        0.0.0.0:8080->8080/tcp    web

С другой машины в сети:

curl.exe -s -o NUL -w "%{http_code}`n" http://192.168.10.30:8080/
200

Страница открывается и снаружи, и на самом хосте через http://localhost:8080. Посмотрим, как это сделано:

Get-HnsEndpoint | Select-Object Name, IPAddress, VirtualNetworkName | Format-List
(Get-HnsEndpoint).Policies | ForEach-Object { [pscustomobject]@{ Type = $_.Type; Protocol = $_.Protocol; InternalPort = $_.InternalPort; ExternalPort = $_.ExternalPort } } | Format-Table
Name               : 7c686c1fbb87c5b10437fcdb3f4f071a469905dfe2b71f962f42b3d798c16396_nat
IPAddress          : 172.30.124.41
VirtualNetworkName : nat

Type Protocol InternalPort ExternalPort
---- -------- ------------ ------------
NAT  TCP              8080         8080

Плагин nat создал для контейнера конечную точку (endpoint) HNS — его сетевой адаптер, подключённый к сети nat, — и повесил на неё политику NAT: соединения на порт 8080 хоста уходят на порт 8080 контейнера. Порт открыт на всех адресах хоста, а правила брандмауэра для него нет (Get-NetFirewallPortFilter не находит порт 8080), как нет и объекта Get-NetNat: трансляцию выполняет HNS, а не служба NAT Windows. Как политики HNS превращаются в правила виртуального коммутатора, разобрано в части 37.


Папки и файлы с хоста

Подключение папки хоста внутрь контейнера (bind mount) у nerdctl выглядит как в Docker — -v <на хосте>:<в контейнере>:

New-Item -ItemType Directory C:\share -Force | Out-Null
'hello from host' | Set-Content C:\share\hello.txt
nerdctl run --rm -v C:\share:C:\data mcr.microsoft.com/windows/nanoserver:ltsc2025 cmd /c "type C:\data\hello.txt & echo written in container> C:\data\from-container.txt"
Get-Content C:\share\from-container.txt
hello from host
written in container

Запись работает в обе стороны: контейнер прочитал файл хоста, а файл, записанный контейнером, появился на хосте. С суффиксом :ro папка подключается только для чтения:

nerdctl run --rm -v C:\share:C:\data:ro mcr.microsoft.com/windows/nanoserver:ltsc2025 cmd /c "echo x> C:\data\ro.txt"
Access is denied.

А теперь то, чего не умел Docker на Windows: подключить отдельный файл. Docker требовал, чтобы источником была папка, и конфигурационный файл приходилось класть в отдельный каталог. containerd с runhcs подключают и файл:

New-Item -ItemType Directory C:\lab -Force | Out-Null
'version=1' | Set-Content C:\lab\app.conf
nerdctl run --rm -v C:\lab\app.conf:C:\config\app.conf mcr.microsoft.com/windows/nanoserver:ltsc2025 cmd /c "dir C:\config & type C:\config\app.conf"
 Volume in drive C has no label.
 Volume Serial Number is AE3E-D6B0

 Directory of C:\config

11/08/2026  02:40 AM    <DIR>          .
11/08/2026  02:39 AM                11 app.conf
               1 File(s)             11 bytes
               1 Dir(s)  21,302,444,032 bytes free
version=1

В ctr то же самое пишется через --mount, с явными параметрами:

ctr run --rm --mount type=bind,src=C:\lab\app.conf,dst=C:\config\app.conf,options=rbind:ro mcr.microsoft.com/windows/nanoserver:ltsc2025 m2 cmd /c "type C:\config\app.conf & echo x>> C:\config\app.conf"
version=1
Access is denied.

Изменения на хосте видны в работающем контейнере сразу — и в папке, и в файле:

$id = nerdctl run -d -v C:\share:C:\data -v C:\lab\app.conf:C:\config\app.conf mcr.microsoft.com/windows/nanoserver:ltsc2025 ping -t localhost
'version=2' | Set-Content C:\lab\app.conf
'new file' | Set-Content C:\share\new.txt
nerdctl exec $id cmd /c "type C:\config\app.conf & dir /b C:\data"
Remove-Item C:\lab\app.conf
'version=3' | Set-Content C:\lab\app.conf
nerdctl exec $id cmd /c "type C:\config\app.conf"
nerdctl rm -f $id
version=2
from-container.txt
hello.txt
new.txt

version=3

Последняя проверка — файл на хосте удалили и создали заново, — и контейнер всё равно видит новое содержимое. Это отличие от Linux: там подключённый файл привязан к конкретному inode, и если редактор сохраняет файл через замену, контейнер продолжает видеть старую версию. На Windows отображение работает по пути. Чем оно сделано — фильтром файловой системы bindflt, — посмотрим в третьей части.


Лимиты памяти и процессора

У контейнера с изоляцией процессов нет своей виртуальной машины, его процессы работают на ядре хоста. Ограничивает их объект задания (job object), в который входит silo контейнера, — механизм Windows, который считает и ограничивает ресурсы группы процессов (подробнее — в части 36). Проверим, работают ли флаги --memory и --cpus. Для этого нужен PowerShell внутри контейнера, а в Nano Server его нет, поэтому возьмём образ Server Core — он заметно тяжелее:

nerdctl pull mcr.microsoft.com/windows/servercore:ltsc2025
nerdctl images
IMAGE                                                             ID              DISK USAGE    CONTENT SIZE    EXTRA
mcr.microsoft.com/dotnet/samples:aspnetapp-nanoserver-ltsc2025    3bbeaca15f52    1.76GB        258MB           U
mcr.microsoft.com/windows/nanoserver:ltsc2025                     05d1a7fb562f    1.28GB        194MB
mcr.microsoft.com/windows/servercore:ltsc2025                     4e5730bddf04    15GB          2.33GB

2,33 ГБ в сжатом виде и 15 ГБ на диске. Буква U в последнем столбце — образ используется контейнером, у нас это web.

Память

Внутри контейнера PowerShell в цикле выделяет массивы по 64 МБ, пока не наберёт 2 ГБ или не получит ошибку:

$img = 'mcr.microsoft.com/windows/servercore:ltsc2025'
$test = '$l = [Collections.Generic.List[byte[]]]::new(); $i = 0; try { while ($i -lt 32) { $l.Add([byte[]]::new(64MB)); $i++ }; "allocated $($i*64) MB, no error" } catch { "failed after $($i*64) MB: $($_.Exception.InnerException.GetType().Name)" }'
nerdctl run --rm $img powershell -NoProfile -Command $test
nerdctl run --rm --memory 512m $img powershell -NoProfile -Command $test
allocated 2048 MB, no error
failed after 320 MB: OutOfMemoryException

Без лимита контейнер спокойно забрал 2 ГБ. С --memory 512m выделение остановилось на 320 МБ массивов: следующий блок в 64 МБ не поместился, потому что часть памяти занимает сам PowerShell с .NET. Обратите внимание, как именно: процесс получил ошибку выделения памяти и сам её обработал, контейнер продолжил работать и завершился штатно. На Linux при превышении лимита ядро убивает процесс (OOM killer), а Windows просто отказывает в новой памяти — с последствиями этого в Kubernetes разбиралась часть 39.

Процессор

Тут проверка сложнее: нужно нагрузить контейнер и посчитать снаружи, сколько процессорного времени он получил. Внутри контейнера запускаем четыре процесса PowerShell с пустым бесконечным циклом — каждый хочет целое ядро, — а на хосте складываем процессорное время процессов контейнера за 10 секунд. Процессы контейнера найдём по номеру сеанса, его показывает hcsdiag list (об этом — в последнем разделе). Скрипт целиком — cpu-test.ps1 в репозитории:

.\cpu-test.ps1
.\cpu-test.ps1 -Cpus 1
limit: none; session 3; 5 powershell processes; CPU used: 3,98 of 4 cores
limit: --cpus 1; session 3; 5 powershell processes; CPU used: 1,04 of 4 cores

Без лимита четыре цикла заняли все четыре ядра машины, с --cpus 1 — одно ядро на всех. Пятый процесс PowerShell — тот, что их запустил и ждёт.

Что попадает в спецификацию

nerdctl не ограничивает ничего сам: он записывает лимиты в спецификацию контейнера — JSON в формате OCI, который containerd передаёт shim, — а shim передаёт их HCS. Спецификацию можно прочитать через ctr:

$id = nerdctl run -d --cpus 1 --memory 512m mcr.microsoft.com/windows/nanoserver:ltsc2025 ping -t localhost
(ctr containers info $id | ConvertFrom-Json).Spec.windows | ConvertTo-Json -Depth 5
nerdctl rm -f $id
{
    "layerFolders": null,
    "resources": {
        "memory": {
            "limit": 536870912
        },
        "cpu": {
            "count": 1
        }
    },
    "ignoreFlushesDuringBoot": true,
    "network": {
        "allowUnqualifiedDNSQuery": true,
        "networkNamespace": "5602c638-42b8-40e5-abe1-8b14ae8af66b"
    }
}

Память — limit в байтах, 512 МиБ. Процессор nerdctl задаёт числом процессоров — count: 1. В спецификации OCI для Windows есть и другие способы: maximum — доля времени всех процессоров в десятитысячных, и shares — относительный вес при конкуренции. kubelet, как мы видели в части 39, переводит limits.cpu пода именно в maximum. Здесь же видно и сетевое пространство имён (networkNamespace) — то самое, которое nerdctl передал CNI-плагину в команде ADD.


Изоляция процессов: что видно с хоста

Все контейнеры в этой части работали с изоляцией процессов — это значение по умолчанию и у runhcs-wcow-process в конфигурации containerd, и у nerdctl:

nerdctl run --help | Select-String isolation
      --isolation string    Specify isolation technology for container. On Linux the only valid value is default. Windows options are host, process and hyperv with process isolation as the default (default "default")

Уровней изоляции три:

  • process — изоляция процессов. Процессы контейнера работают на ядре хоста, но внутри server silo: со своей файловой системой, реестром, сетью и пространством объектов. Этот режим мы и используем.
  • hyperv — изоляция Hyper-V. Контейнер работает в отдельной небольшой виртуальной машине со своим ядром, поэтому ему не обязательно совпадать с хостом по сборке. Нужен компонент Hyper-V, которого на нашем стенде нет, поэтому такой контейнер не запустится. Подробно о нём — в шестой части.
  • host — контейнер HostProcess. Изоляции нет вовсе: процесс работает прямо на хосте, от имени SYSTEM, с его файлами и сетью. В Kubernetes так запускают системные компоненты узла, которым нужен доступ к хосту, например агент сетевого плагина.

Разницу видно сразу:

nerdctl run --rm --isolation host mcr.microsoft.com/windows/nanoserver:ltsc2025 cmd /c "hostname & whoami"
nerdctl run --rm --isolation hyperv mcr.microsoft.com/windows/nanoserver:ltsc2025 cmd /c "hostname"
wincont-lab
nt authority\system
time="2026-11-08T11:41:47+03:00" level=error msg="failed to accept stdout connection on \\\\.\\pipe\\ctr-cdfa883989cdbbcd085a9ddab5472235198beb489e81cedff0026ca5f3bd3342-stdout" error="use of closed network connection"
time="2026-11-08T11:41:47+03:00" level=error msg="failed to accept stderr connection on \\\\.\\pipe\\ctr-cdfa883989cdbbcd085a9ddab5472235198beb489e81cedff0026ca5f3bd3342-stderr" error="use of closed network connection"
time="2026-11-08T11:41:47+03:00" level=fatal msg="failed to create shim task: error in preparing config doc: failed to get host processor information: failed to retrieve processor and processor topology information: hcs::GetServiceProperties: The virtual machine could not be started because a required feature is not installed."

Контейнер host видит имя хоста, а не своё (minwinpc из первого опыта), и работает от SYSTEM, а не от ContainerUser. Контейнеру hyperv HCS не может собрать виртуальную машину без компонента Hyper-V, а строки про каналы stdout и stderr — следствие: задача не создалась, и nerdctl закрыл каналы, к которым никто не подключился. Дальше остаёмся с изоляцией процессов. Убедиться, что контейнеры работают именно в ней, можно по самим контейнерам. hcsdiag — утилита, пришедшая с компонентом Containers, — показывает всё, что создано через HCS:

hcsdiag list
7c686c1fbb87c5b10437fcdb3f4f071a469905dfe2b71f962f42b3d798c16396
    Windows Server Container,   Running,   , containerd-shim-runhcs-v1.exe, 2

Windows Server Container — так HCS называет контейнер с изоляцией процессов; у контейнера с изоляцией Hyper-V тип был бы другим, и рядом появилась бы служебная виртуальная машина. Владелец — shim containerd, последнее число — номер сеанса. Процессы контейнера при изоляции процессов — обычные процессы хоста, только в своём сеансе:

Get-Process | Where-Object SessionId -gt 1 | Sort-Object SessionId, Name | Format-Table Name, Id, SessionId
Name                 Id SessionId
----                 -- ---------
AspNetCoreRazorApp 3784         2
CExecSvc           5436         2
csrss              5284         2
lsass              3756         2
services           4672         2
svchost            5512         2
svchost            5312         2
...
wininit            5528         2

Внутри контейнера — свой маленький Windows: csrss, wininit, services, lsass, набор svchost и CExecSvc, через которую работают exec, а рядом — само приложение. Всё это хост видит в обычном списке процессов. Как из этих процессов, server silo и слоёв образа складывается контейнер, мы разберём в третьей части.


Убираем за собой

Контейнеры, образы и папки этой части больше не понадобятся: в каждой части серии мы создаём всё нужное заново и удаляем в конце.

nerdctl rm -f web
nerdctl rmi mcr.microsoft.com/dotnet/samples:aspnetapp-nanoserver-ltsc2025 mcr.microsoft.com/windows/nanoserver:ltsc2025 mcr.microsoft.com/windows/servercore:ltsc2025
Remove-Item C:\share, C:\lab -Recurse -Force
nerdctl ps -a
nerdctl images
CONTAINER ID    IMAGE    COMMAND    CREATED    STATUS    PORTS    NAMES
IMAGE    ID    DISK USAGE    CONTENT SIZE    EXTRA

Служба containerd, nerdctl и CNI-плагины остаются на сервере.


Всё одним скриптом

Всё, что мы сделали руками, кроме компонента Containers (ему нужна перезагрузка), собрано в install-containerd.ps1: он скачивает containerd, nerdctl и CNI-плагины, сверяет контрольные суммы, раскладывает файлы, пишет config.toml и конфигурацию сети nat и регистрирует службу. Версии и подсеть — параметры:

Install-WindowsFeature Containers
Restart-Computer
# после перезагрузки
.\install-containerd.ps1 -ContainerdVersion 2.4.2 -NerdctlVersion 2.4.2 -CniVersion 0.3.3 -Subnet 172.30.0.0/16 -Gateway 172.30.0.1

Скрипт проверен на чистой машине с той же сборкой Windows, что и в статье.


Источники


Дальше — запускаем два контейнера и учим их находить друг друга по имени.

Оставить комментарий могут только зарегистрированные пользователи.

Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.