How to Implement Blue-Green Deployment on Windows Server 2025
Blue-green deployment is a release strategy that maintains two identical production environments — Blue (currently live) and Green (the new version being deployed to) — and switches traffic between them atomically. The key benefit is near-zero downtime: the switch is a configuration change that can be executed in seconds, and rollback is equally instant. On Windows Server 2025 you can implement blue-green at multiple layers: IIS application pools and sites managed by Application Request Routing (ARR), Windows containers orchestrated by Docker Compose with an nginx proxy, or a combination of both. This guide covers all three approaches, including health checks before switching, rollback procedures, and database migration considerations.
Prerequisites
- Windows Server 2025 with IIS role and ARR 3.0+ installed (for IIS-based approach)
- Docker Desktop or Docker Engine for Windows (for container-based approach)
- PowerShell 7.4+ for the automation scripts
- Network Load Balancing (NLB) or ARR as the ingress layer
- A shared SQL Server instance or database migration tooling (Flyway/Liquibase)
- At least twice the server capacity required to run the application
Step 1: Understanding the Blue-Green Model
In a blue-green setup on a single Windows Server host, you create two IIS sites or application pools (Blue and Green), each listening on different internal ports. A reverse proxy (ARR) sits in front of them and routes all inbound traffic to whichever slot is currently designated Live. The Idle slot receives the new deployment and is validated in isolation before the swap.
# Logical architecture
#
# [Client] --> [ARR on port 443]
# |
# Server Farm: "myapp-farm"
# |
# +-----------+-----------+
# | |
# [Blue: port 8080] [Green: port 8090]
# IIS App Pool: Blue IIS App Pool: Green
# Site: MyApp-Blue Site: MyApp-Green
Step 2: Create Blue and Green IIS Application Pools and Sites
Import-Module WebAdministration
# Create application pools
foreach ($slot in @("Blue", "Green")) {
New-WebAppPool -Name "MyApp-$slot" -ErrorAction SilentlyContinue
Set-ItemProperty "IIS:AppPoolsMyApp-$slot" -Name managedRuntimeVersion -Value "v4.0"
Set-ItemProperty "IIS:AppPoolsMyApp-$slot" -Name processModel.identityType -Value 4
}
# Create the Blue site on port 8080
New-Website -Name "MyApp-Blue" `
-PhysicalPath "D:WebAppsMyApp-Blue" `
-ApplicationPool "MyApp-Blue" `
-Port 8080 `
-IPAddress "*" `
-Force
# Create the Green site on port 8090
New-Website -Name "MyApp-Green" `
-PhysicalPath "D:WebAppsMyApp-Green" `
-ApplicationPool "MyApp-Green" `
-Port 8090 `
-IPAddress "*" `
-Force
# Start both sites
Start-Website -Name "MyApp-Blue"
Start-Website -Name "MyApp-Green"
Step 3: Configure ARR Server Farm for Traffic Switching
Install ARR (Application Request Routing) from the Web Platform Installer or directly as an MSI. Then configure an ARR server farm:
# Create the server farm
Add-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "webFarms" `
-Name "." `
-Value @{ name = "myapp-farm" }
# Add Blue server (initially 100 weight = receives all traffic)
Add-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "webFarms/webFarm[@name='myapp-farm']/server" `
-Name "." `
-Value @{ address = "localhost"; enabled = "true" }
Set-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "webFarms/webFarm[@name='myapp-farm']/server[@address='localhost-blue']" `
-Name "applicationRequestRouting.weight" `
-Value 100
# Add Green server (initially weight 0 = receives no traffic)
Set-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "webFarms/webFarm[@name='myapp-farm']/server[@address='localhost-green']" `
-Name "applicationRequestRouting.weight" `
-Value 0
Step 4: PowerShell Script to Toggle Traffic Between Slots
Encapsulate the traffic switch in a reusable function. This script determines which slot is live, validates the idle slot, then switches weights atomically:
function Switch-DeploymentSlot {
param(
[string]$FarmName = "myapp-farm",
[string]$HealthCheckUrl = "http://localhost:{0}/health",
[int] $BluePort = 8080,
[int] $GreenPort = 8090
)
# Determine current live slot by checking weights
$blueWeight = (Get-WebConfigurationProperty `
-PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "webFarms/webFarm[@name='$FarmName']/server[@address='localhost']" `
-Name "applicationRequestRouting.weight").Value
$livePort = if ($blueWeight -gt 0) { $BluePort } else { $GreenPort }
$idlePort = if ($livePort -eq $BluePort) { $GreenPort } else { $BluePort }
$idleSlot = if ($idlePort -eq $GreenPort) { "Green" } else { "Blue" }
Write-Host "Live slot: port $livePort | Idle slot (deploy target): $idleSlot port $idlePort"
# Health check the idle slot before switching
$healthUrl = $HealthCheckUrl -f $idlePort
try {
$response = Invoke-WebRequest -Uri $healthUrl -UseBasicParsing -TimeoutSec 10
if ($response.StatusCode -ne 200) { throw "Health check returned $($response.StatusCode)" }
Write-Host "Health check passed on idle slot ($healthUrl)"
} catch {
Write-Error "ABORT: Health check failed — $_"
return
}
# Switch: set idle slot weight to 100, live slot to 0
$liveAddress = if ($livePort -eq $BluePort) { "localhost-blue" } else { "localhost-green" }
$idleAddress = if ($idlePort -eq $GreenPort) { "localhost-green" } else { "localhost-blue" }
Set-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "webFarms/webFarm[@name='$FarmName']/server[@address='$idleAddress']" `
-Name "applicationRequestRouting.weight" -Value 100
Set-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "webFarms/webFarm[@name='$FarmName']/server[@address='$liveAddress']" `
-Name "applicationRequestRouting.weight" -Value 0
Write-Host "Traffic switched to $idleSlot slot (port $idlePort)"
}
# Usage
Switch-DeploymentSlot
Step 5: Blue-Green with Windows Containers and Docker Compose
For containerised applications, define both slots as separate services in docker-compose.yml with an nginx proxy that can be hot-reloaded:
# docker-compose.yml
version: "3.9"
services:
app-blue:
image: mycompany/myapp:1.5.0
container_name: myapp-blue
ports:
- "8080:80"
environment:
- ASPNETCORE_ENVIRONMENT=Production
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/health"]
interval: 10s
timeout: 5s
retries: 3
app-green:
image: mycompany/myapp:1.6.0
container_name: myapp-green
ports:
- "8090:80"
environment:
- ASPNETCORE_ENVIRONMENT=Production
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/health"]
interval: 10s
timeout: 5s
retries: 3
proxy:
image: nginx:alpine
ports:
- "443:443"
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/certs:/etc/nginx/certs:ro
depends_on:
- app-blue
- app-green
Toggle the nginx upstream by swapping the config file and reloading (no downtime with nginx -s reload):
# nginx.conf snippet — change upstream target between blue/green
upstream myapp {
server host.docker.internal:8080; # Blue — change to 8090 for Green
keepalive 32;
}
# Switch script
(Get-Content ".nginxnginx.conf") `
-replace "server host.docker.internal:8080", "server host.docker.internal:8090" |
Set-Content ".nginxnginx.conf"
docker exec proxy nginx -s reload
Step 6: Rollback Procedure
Rollback is the same operation as the forward switch — simply call Switch-DeploymentSlot again, or manually set the ARR weight back to the previous live slot. The old version is still running in the previously-live slot and can receive traffic immediately:
# Emergency rollback — force traffic back to Blue
Set-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "webFarms/webFarm[@name='myapp-farm']/server[@address='localhost-blue']" `
-Name "applicationRequestRouting.weight" -Value 100
Set-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "webFarms/webFarm[@name='myapp-farm']/server[@address='localhost-green']" `
-Name "applicationRequestRouting.weight" -Value 0
Write-Host "Rollback complete. Traffic is back on Blue slot."
Step 7: Database Migration Considerations
Blue-green deployments become complex when schema changes are involved. The safest pattern is the expand/contract migration strategy:
- Expand: Deploy a schema migration that adds new columns or tables but does not remove old ones. Both the old (Blue) and new (Green) application versions can run against this schema simultaneously.
- Switch: Perform the blue-green traffic switch as described above.
- Contract: In a subsequent deployment, remove the old columns or tables that the Blue version depended on.
# Run Flyway migration against the shared database before switching slots
flyway `
-url="jdbc:sqlserver://sql01:1433;databaseName=MyAppDB;encrypt=true;trustServerCertificate=true" `
-user="flyway_user" `
-password="$env:DB_MIGRATE_PASS" `
-locations="filesystem:D:WebAppsMyApp-Greendbmigrations" `
migrate
Conclusion
Blue-green deployment on Windows Server 2025 provides a production-grade release strategy with instant rollback capability, whether implemented through IIS and ARR weight switching, nginx upstream reconfiguration for containerised workloads, or a combination of both. The key discipline is keeping both slots warm and healthy at all times, running automated health checks before every switch, and treating database migrations as a separate, backward-compatible step. As your deployment maturity grows, consider layering in canary releases by setting the idle slot weight to a small percentage (5–10%) for a validation window before completing the full switch.