How to Set Up a Build Server with MSBuild on Windows Server 2025
A dedicated build server isolates compilation, testing, and packaging from developer workstations, ensuring that software is always built in a clean, reproducible environment. On Windows Server 2025, MSBuild is the native build engine for .NET Framework, .NET Core, and .NET 5+ projects, and it integrates seamlessly with modern CI/CD systems including Jenkins, TeamCity, Azure DevOps, and GitHub Actions self-hosted runners. This tutorial walks you through silently installing Visual Studio Build Tools, locating and invoking MSBuild, building both .NET Framework and modern .NET projects, restoring NuGet packages, and packaging applications — all from PowerShell, ready to be driven by a CI agent.
Prerequisites
- Windows Server 2025 with PowerShell 5.1 or PowerShell 7.x
- Administrator privileges
- Internet access to download Build Tools and NuGet packages
- At least 10 GB of available disk space for Build Tools installation
- Optional: An existing .NET Framework or .NET solution to build
- Optional: A CI agent (Jenkins, TeamCity, Azure DevOps) installed separately
Step 1: Install Visual Studio Build Tools Silently
Visual Studio Build Tools provides MSBuild, the compilers, and the workload packages needed to build .NET projects — without the full IDE. The installer supports unattended installation via command-line arguments, making it suitable for scripted server provisioning.
# Download the Build Tools bootstrapper
$BtUrl = "https://aka.ms/vs/17/release/vs_BuildTools.exe"
$Installer = "$env:TEMPvs_BuildTools.exe"
Write-Host "Downloading Visual Studio Build Tools..."
Invoke-WebRequest -Uri $BtUrl -OutFile $Installer -UseBasicParsing
# Silent install with the following workloads:
# Microsoft.VisualStudio.Workload.MSBuildTools — core MSBuild + compilers
# Microsoft.VisualStudio.Workload.NetCoreBuildTools — .NET Core/.NET 5+ SDK build support
# Microsoft.VisualStudio.Workload.WebBuildTools — ASP.NET build support
#
# --wait : Wait for installation to finish before returning
# --norestart : Suppress automatic reboot
# --quiet : No UI
# --nocache : Do not keep download cache (save disk space on servers)
Write-Host "Installing Build Tools (this takes 5-15 minutes)..."
$Process = Start-Process -FilePath $Installer -ArgumentList @(
"--quiet",
"--wait",
"--norestart",
"--nocache",
"--add", "Microsoft.VisualStudio.Workload.MSBuildTools",
"--add", "Microsoft.VisualStudio.Workload.NetCoreBuildTools",
"--add", "Microsoft.VisualStudio.Workload.WebBuildTools",
"--add", "Microsoft.Net.Component.4.8.TargetingPack",
"--add", "Microsoft.Net.Component.4.8.SDK"
) -Wait -PassThru -NoNewWindow
if ($Process.ExitCode -eq 0 -or $Process.ExitCode -eq 3010) {
Write-Host "Build Tools installed successfully (exit code: $($Process.ExitCode))."
if ($Process.ExitCode -eq 3010) {
Write-Warning "A reboot is required to complete installation."
}
} else {
Write-Error "Installation failed with exit code: $($Process.ExitCode)"
}
Step 2: Locate MSBuild.exe and Add to PATH
Visual Studio installations use a versioned directory structure. The vswhere.exe utility (installed alongside Visual Studio and Build Tools) reliably discovers the installation path regardless of version or edition.
# vswhere.exe is always installed here
$VSWhere = "${env:ProgramFiles(x86)}Microsoft Visual StudioInstallervswhere.exe"
# Find the MSBuild executable for the latest installed Visual Studio / Build Tools
$MSBuildPath = & $VSWhere `
-latest `
-requires Microsoft.Component.MSBuild `
-find MSBuild**BinMSBuild.exe |
Select-Object -First 1
Write-Host "MSBuild found at: $MSBuildPath"
# Add MSBuild directory to the system PATH permanently
$MSBuildDir = Split-Path $MSBuildPath
$CurrentPath = [System.Environment]::GetEnvironmentVariable("Path", "Machine")
if ($CurrentPath -notlike "*$MSBuildDir*") {
[System.Environment]::SetEnvironmentVariable("Path", "$CurrentPath;$MSBuildDir", "Machine")
}
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine")
# Verify
MSBuild.exe -version
Step 3: Restore NuGet Packages Before Building
Most .NET projects depend on NuGet packages that are not committed to source control. The build server must restore these packages before compiling. For .NET Framework solutions use the standalone nuget.exe; for modern .NET use the dotnet restore command.
# Download the latest nuget.exe (for .NET Framework projects)
$NuGetUrl = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
$NuGetPath = "C:BuildToolsnuget.exe"
New-Item -ItemType Directory -Path "C:BuildTools" -Force | Out-Null
Invoke-WebRequest -Uri $NuGetUrl -OutFile $NuGetPath -UseBasicParsing
# Restore packages for a .NET Framework solution
$SolutionPath = "C:SourceMyAppMyApp.sln"
& $NuGetPath restore $SolutionPath -Verbosity detailed
# For .NET 5+ / .NET Core, dotnet restore is preferred (included in .NET SDK)
# Install the .NET 8 SDK if not already present
winget install Microsoft.DotNet.SDK.8 --silent --accept-source-agreements
dotnet restore "C:SourceMyAppMyApp.sln" --verbosity normal
Step 4: Build a .NET Framework Solution with MSBuild
MSBuild accepts a solution file (.sln) or individual project file (.csproj). The most important properties are Configuration (Debug/Release) and Platform. Additional properties control output paths, verbosity, and logging.
$SolutionPath = "C:SourceMyAppMyApp.sln"
$OutputDir = "C:ArtifactsMyApp"
$LogFile = "C:Logsmsbuild_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
New-Item -ItemType Directory -Path $OutputDir, (Split-Path $LogFile) -Force | Out-Null
MSBuild.exe $SolutionPath `
/p:Configuration=Release `
/p:Platform="Any CPU" `
/p:OutputPath=$OutputDir `
/p:TreatWarningsAsErrors=false `
/p:GenerateDocumentation=true `
/t:Build `
/m ` # Parallel build — uses all available CPU cores
/nologo `
/verbosity:minimal `
/flp:"LogFile=$LogFile;Verbosity=detailed"
if ($LASTEXITCODE -eq 0) {
Write-Host "Build succeeded. Artifacts at: $OutputDir"
} else {
Write-Error "Build failed. Review log: $LogFile"
exit $LASTEXITCODE
}
Step 5: Build .NET Core and .NET 5+ Projects with dotnet msbuild
Modern .NET projects can be built with either the dotnet build wrapper or directly with dotnet msbuild, which passes arguments straight through to the MSBuild engine. Both produce identical output; dotnet build is more ergonomic for most scenarios.
$ProjectPath = "C:SourceModernAppModernApp.csproj"
$RuntimeId = "win-x64"
# dotnet build — wraps MSBuild, handles SDK resolution automatically
dotnet build $ProjectPath `
--configuration Release `
--runtime $RuntimeId `
--self-contained false `
--output "C:ArtifactsModernApp" `
--verbosity minimal `
/p:AssemblyVersion="2.5.0.0" `
/p:FileVersion="2.5.0.$env:BUILD_NUMBER"
# Equivalent using dotnet msbuild directly (same engine, more MSBuild-style syntax)
dotnet msbuild $ProjectPath `
/p:Configuration=Release `
/p:RuntimeIdentifier=$RuntimeId `
/p:SelfContained=false `
/t:Build `
/m `
/verbosity:minimal
Step 6: Understanding MSBuild Project File Targets and Tasks
MSBuild project files are XML. You can add custom Target elements that run before or after the standard build, executing tasks such as copying files, running scripts, or invoking external tools. The following example adds a custom pre-build stamp and a post-build copy task directly inside a .csproj file.
@'
net8.0
MyApplication
MyApplication
enable
enable
$([System.DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"))
'@ | Set-Content -Path "C:SourceModernAppModernApp.csproj" -Encoding UTF8
Step 7: Package the Application
After a successful build, package the application as a NuGet package (for libraries) or a self-contained publish output (for executables and web apps).
# Package a class library as a NuGet package
dotnet pack "C:SourceMyLibraryMyLibrary.csproj" `
--configuration Release `
--output "C:ArtifactsNuGet" `
/p:PackageVersion="1.4.2" `
/p:Authors="Build Server" `
/p:Description="Automated build output"
# Publish a self-contained executable for Windows x64
dotnet publish "C:SourceModernAppModernApp.csproj" `
--configuration Release `
--runtime win-x64 `
--self-contained true `
--output "C:ArtifactsModernApp-SelfContained" `
/p:PublishSingleFile=true `
/p:EnableCompressionInSingleFile=true
# List output artifacts
Get-ChildItem "C:Artifacts" -Recurse | Select-Object FullName, Length | Format-Table
Step 8: Integrate with Jenkins and TeamCity Build Agents
CI systems call MSBuild via the PATH — once Build Tools are installed and MSBuild is on the system PATH, Jenkins (running as a Windows service) and TeamCity agents pick it up automatically. A typical build step command in both systems looks like the following.
# Jenkins "Execute Windows batch command" or "Execute PowerShell" step
# Equivalent of what Jenkins would run:
$env:BUILD_NUMBER = "42" # Injected by Jenkins at runtime
MSBuild.exe "C:JenkinsworkspaceMyAppMyApp.sln" `
/p:Configuration=Release `
/p:Platform="Any CPU" `
/p:AssemblyVersion="1.0.$env:BUILD_NUMBER.0" `
/t:Restore;Build `
/m `
/verbosity:minimal
# TeamCity: Add MSBuild runner step with:
# Build file path: MyApp.sln
# MSBuild version: Visual Studio 2022 (auto-detected from PATH)
# Targets: Restore;Build
# Command line parameters: /p:Configuration=Release /p:Platform="Any CPU" /m
Conclusion
Windows Server 2025 with Visual Studio Build Tools and MSBuild forms a reliable, reproducible build server foundation. By silently installing only the required workload components, placing MSBuild on the system PATH, scripting NuGet restoration, and defining post-build packaging steps, you have a build pipeline that any CI system can drive via a simple command invocation. The combination of MSBuild.exe for .NET Framework projects and dotnet build/dotnet publish for modern .NET covers the full spectrum of Microsoft application types. Next, consider adding code signing to your build pipeline using signtool.exe (also available in the Windows SDK), automated unit test execution with dotnet test, and artifact upload to a NuGet feed or artifact repository such as Azure Artifacts or JFrog Artifactory.