Кому может быть полезно

Тем кто собирает что-то под windows и задумался о версионировании сборочного окружения.

Как ведь бывает, прилетает баг, нужно пересобрать старый релиз, а сборочный агент уже с другим окружением. И даже если вы каким-то образом сохраните нужное окружение, как защитить от его от самих сборок? Делать снапшоты, откатываться, или разворачивать агенты с некого золотого образа и прочее.

Версии окружения поднимаются, само окружение усложняется, требование команд к окружению растут и расходятся. Как тут всем угодить?

Исторически так сложилось, что в linux docker заехал как родной, а вот в windows вроде бы есть, но про него не слышно, на Хабре так уж точно. Поэтому если вы решили это попробовать, то эта статья для вас.

Немного теории

У Microsoft базовый docker image под windows версионируются по билду ядра. Если опустить подробности, у вас должно совпадать ядро хоста и ядро докер образа. Либо ядро докер образа должно быть младше ядра хоста, - и тогда вы сможете запустить контейнер с изоляцией hyper-v.

Изоляция процессов обычно предпочтительней, нет лишней прослойки в виде виртуальной машины, но ядро хоста и ядро базового докер образа должно совпадать.

https://learn.microsoft.com/ru-ru/virtualization/windowscontainers/manage-containers/hyperv-container

Подготовка сборочного агента

Волевым решением выбираете версию 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. И ладно было дело только в этом, вы можете даже не знать об этом и просто собираетесь на том, что загрузили.

Понимание приходит потом, когда новая сборка образа перестает собирать ваш код, так как версия компилятора выше и требует пересобрать зависимости. А у вас ствол уже весь собран на промежуточной версии и все завязаны на эту версию.

DevOps смотрит на ошибки сборки
DevOps смотрит на ошибки сборки

В общем выбираете нужную вам версию и скачиваете инструменты для сборки -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

Сервер для раздачи файлов студии

Нужно ознакомится с

https://learn.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-build-tools?view=vs-2022

https://learn.microsoft.com/en-us/visualstudio/install/create-an-offline-installation-of-visual-studio?view=vs-2022

https://learn.microsoft.com/en-us/visualstudio/install/update-a-network-installation-of-visual-studio?view=vs-2022

https://learn.microsoft.com/en-us/visualstudio/install/create-a-network-installation-of-visual-studio?view=vs-2022

https://learn.microsoft.com/en-us/visualstudio/install/automated-installation-with-response-file?view=vs-2022#configure-the-response-file-used-when-installing-from-a-layout

Если кратко, то поднять видновый сервер с 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)


  1. aamonster
    03.11.2025 12:29

    Либо ядро докер образа должно быть младше ядра хоста

    Младше – в смысле, более новая версия, или всё же более старая?


    1. say_TT_plz Автор
      03.11.2025 12:29

      Действительно, это вводит в заблуждение.

      Версия ядра хоста должна быть выше или равна версии ядра докер образа.


  1. datacompboy
    03.11.2025 12:29

    А это всё только для того чтобы wsl --install не делать, да? :)


    1. say_TT_plz Автор
      03.11.2025 12:29

      Не совсем, это для windows на ядре windows для запуска в окружении windows. Так бывает, да, ну или я сарказм не выкупил.


  1. zzzzzzerg
    03.11.2025 12:29

    За ложку дегтя спасибо! Столкнулся с этим, но не разобрался в причинах.