
Кому может быть полезно
Тем кто собирает что-то под windows и задумался о версионировании сборочного окружения.
Как ведь бывает, прилетает баг, нужно пересобрать старый релиз, а сборочный агент уже с другим окружением. И даже если вы каким-то образом сохраните нужное окружение, как защитить от его от самих сборок? Делать снапшоты, откатываться, или разворачивать агенты с некого золотого образа и прочее.
Версии окружения поднимаются, само окружение усложняется, требование команд к окружению растут и расходятся. Как тут всем угодить?
Исторически так сложилось, что в linux docker заехал как родной, а вот в windows вроде бы есть, но про него не слышно, на Хабре так уж точно. Поэтому если вы решили это попробовать, то эта статья для вас.
Немного теории
У Microsoft базовый docker image под windows версионируются по билду ядра. Если опустить подробности, у вас должно совпадать ядро хоста и ядро докер образа. Либо ядро докер образа должно быть младше ядра хоста, - и тогда вы сможете запустить контейнер с изоляцией hyper-v.
Изоляция процессов обычно предпочтительней, нет лишней прослойки в виде виртуальной машины, но ядро хоста и ядро базового докер образа должно совпадать.
Подготовка сборочного агента
Волевым решением выбираете версию windows и привязываете к ней базовый docker image. В нашем примере это будет
Windows Server 2022 Standard 21H2 20348.2849
и
mcr.microsoft.com/windows/server:10.0.20348.2966-amd64
Разворачиваем OS и устанавливаем на ней docker скриптом https://learn.microsoft.com/ru-ru/virtualization/windowscontainers/quick-start/set-up-environment?tabs=dockerce#windows-server-1
Нас интересует Docker CE, это самый классический вариант, с понятными командами docker build и docker run.
Устанавливается скриптом, но можно отдельно выкачать бинарь и зарегать службу вручную. Вся служба умещается в один dockerd.exe и docker.exe которые копируется в system32.
К нему идут виндовые фичи, если устанавливаете вручную, то их стоит установить.
HypervisorPlatform
VirtualMachinePlatform
Containers
Первый запуск службы docker, создаст каталог C:\ProgramData\docker, который будет содержать настройки, слои и кэш, что нас не устраивает.
Поэтому правим "C:\ProgramData\docker\config\daemon.json".
Но перед этим создаем отдельный диск под докер, пусть будет E:\
Запускам powershell от имени админа, подставляем в скрипт url вашего локального докер реестра и выполняем команды:
Stop-Service docker -ErrorAction stop
$json = @{
"data-root" = "E:\docker"
"allow-nondistributable-artifacts" = "{{url вашего локального docker registry|harbor.domen.local}}"
"exec-opts" = "isolation=process"
"group" = "docker"
"storage-opts" = "size=200GB"
} |ConvertTo-Json -Depth 3
$json |out-file -FilePath "C:\ProgramData\Docker\config\daemon.json" -Encoding utf8 -Force
New-LocalGroup -name "docker"
Start-Service docker -ErrorAction stop
"data-root" = "E:\docker" - там где мы будем все хранить.
allow-nondistributable-artifacts - чтобы можно было пушить проприетарные слои мелкомягких к себе в реестр.
"exec-opts" = "isolation=process" - изоляция процессов
"group" = "docker" - группа пользователей, которая может запускать докер
storage-opts - выделяется по дефолту 30gb или около того при запуске контейнера, не всегда помещается при сборке.
Тут есть одна неочевидная вещь которая мне в будущем сильно пригодилась. Чистить от мусора очень долго, но отформатировать диск дело пары секунд. То есть мы останавливаем службу докер, форматируем диск и снова запускаем.
Сборка образа
В целом, все тоже самое и тот же синтаксис, за исключением, что вам придется оперировать CMD оболочкой или PowerShell. Пошик мне привычнее поэтому буду работать в нем.
#классический Dockerfile из примера microsoft
# escape=`
# Use the latest Windows Server Core 2022 image.
FROM mcr.microsoft.com/windows/servercore:ltsc2022
# Restore the default Windows shell for correct batch processing.
SHELL ["cmd", "/S", "/C"]
RUN `
# Download the Build Tools bootstrapper.
curl -SL --output vs_buildtools.exe https://aka.ms/vs/17/release/vs_buildtools.exe `
`
# Install Build Tools with the Microsoft.VisualStudio.Workload.AzureBuildTools workload, excluding workloads and components with known issues.
&& (start /w vs_buildtools.exe --quiet --wait --norestart --nocache `
--installPath "%ProgramFiles(x86)%\Microsoft Visual Studio\2022\BuildTools" `
--add Microsoft.VisualStudio.Workload.AzureBuildTools `
--remove Microsoft.VisualStudio.Component.Windows10SDK.10240 `
--remove Microsoft.VisualStudio.Component.Windows10SDK.10586 `
--remove Microsoft.VisualStudio.Component.Windows10SDK.14393 `
--remove Microsoft.VisualStudio.Component.Windows81SDK `
|| IF "%ERRORLEVEL%"=="3010" EXIT 0) `
`
# Cleanup
&& del /q vs_buildtools.exe
# Define the entry point for the docker container.
# This entry point starts the developer command prompt and launches the PowerShell shell.
ENTRYPOINT ["C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\Common7\\Tools\\VsDevCmd.bat", "&&", "powershell.exe", "-NoLogo", "-ExecutionPolicy", "Bypass"]
Мне он не подошел по многим причинам.
Нужен интернет
https://aka.ms/vs/17/release/vs_buildtools.exe — самый большой обман и я позже объясню почему
mcr.microsoft.com/windows/servercore:ltsc2022 — часто не может сжать слой при билде если брать его как базовый. Не знаю точно с чем связано, но просто server хоть и толще но он всегда собирается.
По поводу https://aka.ms/vs/17/release/vs_buildtools.exe, внезапно VS 2022 тоже версионируется внутри продукта. У версий есть свой жизненный цикл.
Идем сюда https://learn.microsoft.com/ru-ru/visualstudio/releases/2022/release-history и видим:

У нас оказывается есть LTSC версии, есть промежуточные и сроки поддержки.
Тут https://aka.ms/vs/17/release/vs_buildtools.exe мы берем текущую версию и 50/50 вы попадаете на нечетную версию которая не LTSC. И ладно было дело только в этом, вы можете даже не знать об этом и просто собираетесь на том, что загрузили.
Понимание приходит потом, когда новая сборка образа перестает собирать ваш код, так как версия компилятора выше и требует пересобрать зависимости. А у вас ствол уже весь собран на промежуточной версии и все завязаны на эту версию.

В общем выбираете нужную вам версию и скачиваете инструменты для сборки -vs_buildtools.exe.
Как из него сделать установщик по сети опишу в конце статьи чтобы не мешать всё в кучу.
Итак нам нужен Dockerfile
Скрытый текст
# escape=`
# Powered by Say_TT_PLZ
ARG base_image=mcr.microsoft.com/windows/server:10.0.20348.2966-amd64
FROM $base_image
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
WORKDIR c:\build
COPY response.json c:/build
RUN curl.exe -SL --output vs_buildtools.exe http://vs-cache.local/vs/buildtools2022_17_12/vs_setup.exe; `
new-item -ItemType Directory -Path C:\build\CA; `
curl.exe -SL --output C:\build\CA\manifestRootCertificate.cer http://vs-cache.local/vs/buildtools2022_17.12/certificates/manifestRootCertificate.cer; `
curl.exe -SL --output C:\build\CA\vs_installer_opc.RootCertificate.cer http://vs-cache.local/vs/buildtools2022_17.12/certificates/vs_installer_opc.RootCertificate.cer; `
curl.exe -SL --output C:\build\CA\manifestCounterSignRootCertificate.cer http://vs-cache.local/vs/buildtools2022_17.12/certificates/manifestCounterSignRootCertificate.cer; `
curl.exe -SL --output C:\build\CA\Microsoft_Windows_Code_Signing_PCA_2024.crt http://vs-cache.local/vs/buildtools2022_17.12/certificates/Microsoft_Windows_Code_Signing_PCA_2024.crt; `
Get-ChildItem C:\build\CA | Import-Certificate -CertStoreLocation Cert:\\currentuser\\CA; `
Get-ChildItem C:\build\CA | Import-Certificate -CertStoreLocation Cert:\\LocalMachine\\CA; `
Get-ChildItem C:\build\CA | Import-Certificate -CertStoreLocation Cert:\\LocalMachine\\Root; `
Start-Process -Wait -NoNewWindow -FilePath C:\build\vs_buildtools.exe -ArgumentList '--in c:\build\response.json --layoutUri http://vs-cache.local/vs/buildtools2022_17.12/ --wait --installPath C:\BuildTools\'; `
Remove-Item -Path "$env:temp\*" -Recurse -Force; `
if (Test-Path "c:\temp"){Remove-Item -Path "c:\temp\*" -Recurse -Force}; `
if (Test-Path 'C:\ProgramData\Package Cache') {Remove-Item -Path 'C:\ProgramData\Package Cache' -Recurse -Force}; `
GCI 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\*.*' -Force | ? {$_.name -ne 'vswhere.exe'} | Remove-Item -Force; `
GCI 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\*' -Force | ? {$_.Mode -eq 'd-----'} | Remove-Item -Force -Recurse; `
GCI c:\build | Remove-Item -Force;
#далее вы можете установить дополнительные пакеты и сделать настройки, в том числе установить SDK WDK, но одно тянет другое и сильно перегружает статью.
Docker при чтении съедает двойные кавычки, наверное их можно как-то экранировать, но либо обходитесь без них, либо используйте одинарные, либо оборачивайте в скрипты.
Если вы планируете устанавливать WDK, у вас из коробки не будет устанавливаться vsix пакеты которые студия автоматом ставит при установке WDK. Так что ручками, я на коленке написал скрипт который это делает.
Скрытый текст
if (!(test-path "C:\Temp\wdk")){new-item -ItemType Directory -Path "C:\Temp\wdk"}
$temp_folder = "c:\temp\wdk\new"
if (Test-Path "C:\Program Files (x86)\Windows Kits\10\Vsix\VS2022"){$Vsix_2022 = $(GCI "C:\Program Files (x86)\Windows Kits\10\Vsix\VS2022")[0].fullname + "\WDK.vsix"}
else{$Vsix_2022 = $false}
$vsix_2017 = @(
@{
wdk_path = "C:\Program Files (x86)\Windows Kits\10\Vsix\WDK.vsix"
build_tools_path = "C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools"
},
@{
wdk_path = "C:\Program Files (x86)\Windows Kits\10\Vsix\WDK.vsix"
build_tools_path = "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools"
}
)
$vsix_msbuild = @(
@{
wdk_path = "C:\Program Files (x86)\Windows Kits\10\Vsix\VS2019\WDK.vsix"
build_tools_path = "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools"
},
@{
wdk_path = $Vsix_2022
build_tools_path = "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools"
}
)
foreach ($vsix in $vsix_2017){
if ((test-path $vsix.wdk_path) -and (test-path $vsix.build_tools_path)){
if (!(test-path $temp_folder)){new-item -ItemType Directory -Path $temp_folder}
copy-item -path $vsix.wdk_path -Destination "$temp_folder\WDK.zip"
Expand-Archive -Path "$temp_folder\WDK.zip" -DestinationPath "$temp_folder"
$source_path = "$temp_folder" + '\$VCTargets'
$source_path_true = "$temp_folder" + '\VCTargets'
Rename-Item -Path $source_path -NewName $source_path_true
$destination_path = $vsix.build_tools_path + "\Common7\IDE\VC\VCTargets"
copy-item -path "$source_path_true\*" -Destination $destination_path -Recurse -Force
Remove-Item "$temp_folder" -Recurse -Force
}
}
foreach ($vsix in $vsix_msbuild){
if ((test-path $vsix.wdk_path) -and (test-path $vsix.build_tools_path)){
if (!(test-path $temp_folder)){new-item -ItemType Directory -Path $temp_folder}
copy-item -path $vsix.wdk_path -Destination "$temp_folder\WDK.zip"
Expand-Archive -Path "$temp_folder\WDK.zip" -DestinationPath "$temp_folder"
$source_path = "$temp_folder" + '\$MSBuild'
$source_path_true = "$temp_folder" + '\MSBuild'
Rename-Item -Path $source_path -NewName $source_path_true
$destination_path = $vsix.build_tools_path + "\MSBuild"
copy-item -path "$source_path_true\*" -Destination $destination_path -Recurse -Force
Remove-Item "$temp_folder" -Recurse -Force
}
}Только build_tools_path - у вас должен быть актуальный, в скрипте он ссылается на оригинальный путь вроде C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools что требует кавычек, а в Dockerfile я использую другой путь. (Вообще, у себя я ставлю через скрипты, но в статье сильно все упростил)
Сертификат который внезапно требуется, но не идет с поставкой
https://www.microsoft.com/pkiops/certs/Microsoft Windows Code Signing PCA 2024.crt
Сервер для раздачи файлов студии
Нужно ознакомится с
Если кратко, то поднять видновый сервер с IIS и настроить его для раздачи файлов по сети.
Чтобы облегчить вам труд, выставите параметры
MIME TYPES
.vsix |
application/octet-stream |
.ps1 |
application/postscript |
.opc |
application/octet-stream |
.nupkg |
application/octet-stream |
.msu |
application/octet-stream |
.cer |
application/octet-stream |
Далее сделайте offline инсталятор с помощью скачанного vs_buildtools.exe в самом начале статьи и перечислением всех нужных вам компонентов включая рекомендованные.
Опубликуйте полученный каталог с помощью IIS.
response.json
То, что скачаете все нужные файлы студии не значит, что вы их все установите. Тем более нужно описывать в виде кода, что вы устанавливаете. И тут нам пригодится response.json.
Скрытый текст
{
"installChannelUri": ".\\ChannelManifest.json",
"channelUri": "http://vs-cache.local/vs/buildtools2022_17_12/ChannelManifest.json",
"installCatalogUri": ".\\Catalog.json",
"channelId": "VisualStudio.17.Release",
"productId": "Microsoft.VisualStudio.Product.BuildTools",
"quiet": true,
"passive": false,
"norestart": true,
"noUpdateInstaller": true,
"nocache": true,
"includeRecommended": false,
"add": [
"Microsoft.VisualStudio.Workload.ManagedDesktopBuildTools",
"Microsoft.NetCore.Component.SDK",
"Microsoft.VisualStudio.Workload.MSBuildTools",
"Microsoft.VisualStudio.Workload.VCTools",
"Microsoft.VisualStudio.ComponentGroup.NativeDesktop.Llvm.Clang",
"Microsoft.Component.VC.Runtime.UCRTSDK",
"Microsoft.VisualStudio.Component.TestTools.BuildTools",
"Microsoft.VisualStudio.Component.VC.ASAN",
"Microsoft.VisualStudio.Component.VC.CMake.Project",
"Microsoft.VisualStudio.Component.VC.Llvm.Clang",
"Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset",
"Microsoft.VisualStudio.Component.VC.ATL",
"Microsoft.VisualStudio.Component.VC.ATL.Spectre",
"Microsoft.VisualStudio.Component.VC.ATLMFC",
"Microsoft.VisualStudio.Component.VC.ATLMFC.Spectre",
"Microsoft.VisualStudio.Component.VC.CLI.Support",
"Microsoft.VisualStudio.Component.VC.MFC",
"Microsoft.VisualStudio.Component.VC.MFC.Spectre",
"Microsoft.VisualStudio.Component.VC.x86.x64",
"Microsoft.VisualStudio.Component.VC.x86.x64.Spectre"
],
"addProductLang": [
"en-US"
]
}По сути все тоже самое что вы передаете в виде параметров, но в более удобном формате.
Так вы наглядно видите какие компоненты вы установили и чего вам может не хватать.
Ложка дегтя
Вы обязательно столкнетесь с ошибкой
hcsshim::ImportLayer failed in Win32: The system cannot find the path specified. (0x3)
Полезете в гугл и наткнетесь на ишак https://github.com/microsoft/hcsshim/issues/835
Используйте mcr.microsoft.com/windows/server вместо mcr.microsoft.com/windows/servercore
Не устанавливайте cygwin, и если устанавливаете, то перепаковывайте его. Жесткие ссылки которые он создает при установке не дают сжать слой.
Иногда ошибка появляется если какой-то из слоев сломан, помогает docker zap или самое простое это форматнуть диск на котором вы храните data-root. Всегда помогает и это очень быстро.
Если ставите build tools, всегда указывайте путь установки --installPath, даже если это путь по умолчанию. Без этой опции почти наверняка получите ошибку.
Ну и да, докер образ будет жирным, так что не пытайтесь установить всё, что может понадобится, ставьте только нужное.
Я не разобрал в статье как безопасно передавать креды, ответ никак. Ну почти, я для этого создаю временную smb шару на хосте и маплю её в докер при сборке и оттуда уже беру нужные зашифрованные креды. Вы можете придумать свой велосипед. В теории вы можете установить на винде отдельно containerd и buildkit, - они есть под винду и собирать на них, но я не осилил. В том числе потому, что buildkit смотрит в манифест, даже если тянет его с локального реестра, а там пути ведут в mcr.microsoft.com, а интернета нет и он получает отлуп.
Но через тернии к версионированию, может windows и утрачивает свои позиции, но я много страдал и возможно благодаря этой статье вам придется делать это меньше. Хоть она и получилась сумбурной.
Комментарии (5)

datacompboy
03.11.2025 12:29А это всё только для того чтобы
wsl --installне делать, да? :)
say_TT_plz Автор
03.11.2025 12:29Не совсем, это для windows на ядре windows для запуска в окружении windows. Так бывает, да, ну или я сарказм не выкупил.
aamonster
Младше – в смысле, более новая версия, или всё же более старая?
say_TT_plz Автор
Действительно, это вводит в заблуждение.
Версия ядра хоста должна быть выше или равна версии ядра докер образа.