0. 背景
鑒于目前公司的舊項(xiàng)目是.NET4.5開(kāi)發(fā)的,為方便部署,打算使用docker的Windows Container做一個(gè)打包鏡像
目前基于Windows Container的例子太少,所以也確實(shí)踩了不少坑,這里記錄一下
1. 操作系統(tǒng)版本
Windows家庭版是可以安裝Docker,啟動(dòng)并成功運(yùn)行Linux Container。
但如果要切換到Windows Container將會(huì)卡在啟動(dòng)階段,且不會(huì)有任何報(bào)錯(cuò)!

由于剛換了筆記本,平時(shí)也沒(méi)注意系統(tǒng)版本,這個(gè)問(wèn)題卡了1天,中間嘗試了各種命令,配置服務(wù),注冊(cè)表都無(wú)效;
最后切換到“Windows專(zhuān)業(yè)版”問(wèn)題解決。
如果遇到卡在啟動(dòng)階段一直無(wú)法成功啟動(dòng)的,不妨換系統(tǒng)版本試一試。
2. Git版本
因?yàn)槟壳暗膚indows sdk鏡像中不包含winget或choco所以如果需要額外的軟件支持是需要自己打包鏡像的時(shí)候安裝進(jìn)去的。
當(dāng)時(shí)下載了git官網(wǎng)最新的2.38.0,在本地跑的好好的一段腳本,在docker直接卡死無(wú)響應(yīng)。

在網(wǎng)上搜了一圈,偶爾發(fā)現(xiàn)一篇文章,說(shuō)是git版本太低導(dǎo)致命令卡死,但是,我下的是最新版本?。?/strong>,抱著試一試想法,把git版本退回本地版本
2.34.1.windows.1,問(wèn)題解決。
另外使用git還需要對(duì)git進(jìn)行一些安全方面的配置,這個(gè)網(wǎng)上可以隨便找到
@rem build.bat # 設(shè)置git安全策略 RUN git config --global --add safe.directory "*" RUN git config --system http.sslverify false
3. 設(shè)置nuget緩存
做過(guò)CI的同學(xué)肯定都知道nuget必須是要緩存的,否則每次構(gòu)建都需要完整的下載全部nuget依賴(lài),會(huì)很慢。
.net core可以通過(guò)dotnet restore --packages "..."的方式指定緩存文件夾,但dotnet restore命令在framework的項(xiàng)目中無(wú)法使用,只能通過(guò)nuget.exe的來(lái)還原,他們的設(shè)置方式不一樣。
需要通過(guò)命令設(shè)置全局緩存文件夾:nuget config -set globalPackagesFolder=c:/nuget repositoryPath=c:/nuget,這一步可以放在Dockerfile中執(zhí)行
4. 安裝NodeJs
Web項(xiàng)目構(gòu)建可能需要依賴(lài)NodeJs,但微軟的SDK鏡像中并不包含,需要自己安裝。

安裝方式:
node-v16.17.1-x64.msi
###Dockerfile
COPY ./node-v16.17.1-x64.msi c:/bat/node-v16.17.1-x64.msi
# 安裝nodejs
RUN msiexec /a "C:\bat\node-v16.17.1-x64.msi" /qb TARGETDIR="C:\nodejs"
# 設(shè)置環(huán)境變量
RUN setx path '%path%;C:/nodejs/nodejs'
5. NuGet私倉(cāng)鑒權(quán)
這個(gè)問(wèn)題我還不知道發(fā)生的原因,現(xiàn)象就是:
將nuget賬號(hào)密碼配置在Dockerfile階段會(huì)失效,在編譯時(shí)依然顯示需要賬號(hào)密碼鑒權(quán)
目前解決方案是將配置操作放在編譯階段
### build.bat
nuget source remove -name mynuget
nuget source add -source http://mynuget/nuget-hosted/ -Name mynuget -Username mynuget -Password mynuget
6. NuGet還原失敗1
這是一個(gè)已知的bug,它會(huì)阻止nuget restore的操作,顯示全部項(xiàng)目已還原,但實(shí)際上并沒(méi)有 具體看這里
解決方法是在 *.csproj 文件中刪除類(lèi)似下面這樣的代碼
<!-- web.config -->
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>這臺(tái)計(jì)算機(jī)上缺少此項(xiàng)目引用的 NuGet 程序包。使用“NuGet 程序包還原”可下載這些程序包。有關(guān)更多信息,請(qǐng)參見(jiàn) http://go.microsoft.com/fwlink/?LinkID=322105。缺少的文件是 {0}。</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\Microsoft.TypeScript.MSBuild.4.6.4\build\Microsoft.TypeScript.MSBuild.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.TypeScript.MSBuild.4.6.4\build\Microsoft.TypeScript.MSBuild.props'))" />
<Error Condition="!Exists('..\packages\Microsoft.TypeScript.MSBuild.4.6.4\build\Microsoft.TypeScript.MSBuild.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.TypeScript.MSBuild.4.6.4\build\Microsoft.TypeScript.MSBuild.targets'))" />
<Error Condition="!Exists('..\packages\EntityFramework.6.4.4\build\EntityFramework.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\EntityFramework.6.4.4\build\EntityFramework.props'))" />
<Error Condition="!Exists('..\packages\EntityFramework.6.4.4\build\EntityFramework.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\EntityFramework.6.4.4\build\EntityFramework.targets'))" />
</Target>
7. NuGet還原失敗2
如果項(xiàng)目引用關(guān)系比較復(fù)雜,msbuild通過(guò)單個(gè)csproj文件分析項(xiàng)目關(guān)系還原依賴(lài)會(huì)出現(xiàn)問(wèn)題;
nuget restore "%csproj%" -PackagesDirectory "packages" -NonInteractive -Recursive
之后可能會(huì)出現(xiàn)編譯失敗的情況
解決方案:通過(guò)sln項(xiàng)目文件還原依賴(lài)沒(méi)事
nuget restore "%sln%" -PackagesDirectory "packages" -NonInteractive
8. Access is denied.
這個(gè)問(wèn)題我也沒(méi)搞懂是為什么,目前觀(guān)察到的現(xiàn)象就是:
編譯項(xiàng)目A,A引用了B,使用
msbuild -r "A項(xiàng)目目錄" -property:OutDir="輸出目錄";編譯A項(xiàng)目,同時(shí)會(huì)將B項(xiàng)目也輸出到輸出目錄,此時(shí)在docker中無(wú)法操作B項(xiàng)目輸出的文件,刪除,移動(dòng),重命名等操作均返回Access is denied 。
Access is denied
重新運(yùn)行一個(gè)容器可以正常操作;
起初以為是有進(jìn)程鎖文件,但使用tasklist沒(méi)有觀(guān)察到異常進(jìn)程;
再次懷疑是權(quán)限不足,使用takeown提升權(quán)限,顯示成功,但依然無(wú)法操作。
這種情況在每次編譯一個(gè)項(xiàng)目時(shí)并不會(huì)有什么太大的問(wèn)題,但如果一次編譯多個(gè)項(xiàng)目,且項(xiàng)目之間有引用關(guān)系時(shí),編譯第二個(gè)項(xiàng)目就會(huì)出現(xiàn)因?yàn)槲募o(wú)權(quán)限操作而編譯失敗的情況。
目前的解決方案是,編譯項(xiàng)目到各自項(xiàng)目的bin目錄,最后再xcopy到發(fā)布目錄
@rem build.bat
msbuild -r "%~1" -verbosity:n -property:WarningLevel=0;OutDir=.\bin\;Configuration=%MODE%
if exist "%~p1bin\_PublishedWebsites\%~n1" (
call :Exec "xcopy /seyfrhq "%~p1bin\_PublishedWebsites\%~n1" C:\publish\%~n1"
) else (
if exist "%~p1bin\_PublishedWebsites" (
call :Exec "xcopy /seyfrhq "%~p1bin\_PublishedWebsites" C:\publish\%~n1"
) else (
call :Exec "xcopy /seyfrhq "%~p1bin" C:\publish\%~n1"
)
)
9. 多個(gè)分支并發(fā)構(gòu)建
由于實(shí)際場(chǎng)景中項(xiàng)目較大,考慮到每次都從git拉取整個(gè)項(xiàng)目時(shí)間較長(zhǎng),所以選擇掛載項(xiàng)目目錄到docker容器中;
但這就出現(xiàn)了多個(gè)Runner同時(shí)構(gòu)建不同分支時(shí),必然會(huì)互相影響。
解決方案是:將.git文件夾復(fù)制到docker容器內(nèi)部,然后執(zhí)行還原+切換分支,再進(jìn)行構(gòu)建,這樣即使多進(jìn)程同時(shí)構(gòu)建也不會(huì)互相影響。
@rem build.bat
cd "c:\src"
mkdir C:\source\.git & xcopy /seyfrhqk "c:\src\.git" "c:\source\.git"
mkdir C:\source\packages & xcopy /seyfrhqk "c:\src\packages" "c:\source\packages
cd "c:\source"
git clean -dfx -q -e web.config -e packages & git reset --hard & git reset HEAD . & git checkout .
git checkout %BRANCH% && git pull origin %BRANCH% -q
10. build.bat
echo off
if exist "c:\bat\DEBUG" if "%1"=="" (
echo "DEBUG MODE"
cmd
exit /b
)
call :Exec "rd /s /q c:\publish"
cd "c:\src"
if "%MODE%"=="" set MODE=DEBUG
if "%BRANCH%"=="" set BRANCH=master
echo "=================================================================="
echo "==== bat version 2022-10-07 ======================================"
echo "==== 2022-10-07 14:54:48 ======================================"
echo "============ dir ================================================="
echo "BRANCH = %BRANCH%"
echo "MODE = %MODE%"
echo "SLN = %SLN%"
echo "CSPROJ = %CSPROJ%"
echo "=================================================================="
call :Exec "git fetch --all"
call :Exec "mkdir C:\source\.git & xcopy /seyfrhqk "c:\src\.git" "c:\source\.git""
@REM call :Exec "mkdir C:\source\packages & xcopy /seyfrhqk "c:\src\packages" "c:\source\packages""
call :Exec "cd "c:\source""
call :Exec "git clean -dfx -q -e web.config -e packages & git reset --hard & git reset HEAD . & git checkout ."
call :Exec "git checkout %BRANCH% && git pull origin %BRANCH% -q"
for /r c:\src %%i in (web.config) do (
if exist %%i (
call :CopyToSource %%i
)
)
@REM 如果有私倉(cāng),這里進(jìn)行配置
@REM call :Exec "nuget source list |findstr mynuget && nuget source remove -name mynuget"
@REM call :Exec "nuget source add -source http://mynuget/ -Name mynuget -Username mynuget -Password mynuget"
if "%SLN%"=="" (
call :Exec "nuget restore "c:\source" -PackagesDirectory "packages" -NonInteractive"
) else (
call :Exec "nuget restore "c:\source\%SLN%" -PackagesDirectory "packages" -NonInteractive"
)
call :ResolvePath %CSPROJ% CSPROJ
set entry=%CSPROJ%
:loop
for /f "tokens=1* delims=;" %%a in ("%entry%") do (
echo %%a
call :ResolvePath %%a entry
call :Build %entry%
set entry=%%b
)
if defined entry goto :loop
rem === Functions ===
exit /b
:ResolvePath
set %2=%~dpfn1
exit /b
:Exec
echo "******************************************************************"
echo "> %~1"
call %~1
exit /b
:Build
mkdir "C:\publish\%~n1"
@REM call :Exec "nuget restore "%~1" -PackagesDirectory "packages" -NonInteractive -Recursive"
call :Exec "msbuild -r "%~1" -verbosity:n -property:WarningLevel=0;OutDir=.\bin\;Configuration=%MODE%"
call :Exec "taskkill /f /im VBCSCompiler.exe /t"
if exist "%~p1bin\_PublishedWebsites\%~n1" (
call :Exec "xcopy /seyfrhq "%~p1bin\_PublishedWebsites\%~n1" C:\publish\%~n1"
) else (
if exist "%~p1bin\_PublishedWebsites" (
call :Exec "xcopy /seyfrhq "%~p1bin\_PublishedWebsites" C:\publish\%~n1"
) else (
call :Exec "xcopy /seyfrhq "%~p1bin" C:\publish\%~n1"
)
)
:CopyToSource
set src=%~1
set dest=%src:c:\src=c:\source%
echo F|xcopy /seyfrhqk "%src%" "%dest%"
exit /b
11. Dockerfile
FROM mcr.microsoft.com/dotnet/framework/sdk:4.8-windowsservercore-ltsc2019
WORKDIR "C:/"
COPY ./git "C:/git"
COPY ./build.bat c:/bat/build.bat
COPY ./node-v16.17.1-x64.msi c:/bat/node-v16.17.1-x64.msi
# COPY ["./NETFramework", "C:/Program Files (x86)/Reference Assemblies/Microsoft/Framework/.NETFramework"]
RUN Write-Output 2022-10-07 >/imgver
# 創(chuàng)建基礎(chǔ)文件夾
RUN "md c:/source/;md c:/publish/;md c:/nuget/"
# 安裝nodejs
RUN msiexec /a "C:\bat\node-v16.17.1-x64.msi" /qb TARGETDIR="C:\nodejs"
# 設(shè)置環(huán)境變量
RUN setx path '%path%;C:/git/bin;C:/nodejs/nodejs'
# 設(shè)置nuget緩存文件夾
RUN nuget config -set globalPackagesFolder=c:/nuget repositoryPath=c:/nuget
ENV NUGET_HTTP_CACHE_PATH="c:/nuget"
# 設(shè)置git安全策略
RUN git config --global --add safe.directory "*"
RUN git config --system http.sslverify false
# 聲明鏡像變量
ENV MODE=DEBUG CSPROJ= BRANCH=main SLN=
# 啟動(dòng)腳本
ENTRYPOINT ["/bat/build.bat"]
ONBUILD RUN echo 'base image build time : 2022-10-07 14:53:22'
編譯鏡像
cls && docker rmi -f build && docker build . -t build
構(gòu)建.NET項(xiàng)目
docker run -it --rm ^
-v "D:/codes/dotnet/CSM":"c:/src" ^
-v "D:/publish":"c:/publish" ^
-v "D:/apps/.nuget":"c:/nuget" ^
-e CSPROJ="WebUI/WebUI.csproj;Api/Api.csproj" ^
-e MODE="DEBUG" ^
-e BRANCH="release/v1.0" ^
build
