CI/CD Moderno para Umbraco Usando Azure DevOps & IIS On-Premises (Actualización 2025)
Hace algunos años compartí una guía paso a paso sobre cómo desplegar sitios Umbraco en servidores on-premises utilizando los clásicos Release Pipelines de Azure DevOps. Desde entonces, muchas cosas han cambiado.
Hoy, Umbraco se ejecuta sobre .NET moderno, la mayoría de los proyectos utiliza Vite o Tailwind para el tooling frontend, y Azure DevOps impulsa claramente el uso de pipelines en YAML en lugar de los antiguos releases configurados mediante interfaz gráfica.
Esta guía actualizada refleja cómo construimos y entregamos sitios web Umbraco en 2025: utilizando automatización total, despliegues sensibles al entorno y un único pipeline YAML que realiza el deploy a UAT y Producción en servidores IIS dentro de tu propio datacenter.
Si estás modernizando un proceso antiguo o empezando desde cero, este artículo te ofrece una base sólida. Pero recuerda: esto es un boilerplate. Cada organización tiene sus propias necesidades — balanceo de carga, replicación, estrategias de media compartida, gestión de secretos, refuerzo de seguridad y gobernanza de despliegues. Casi siempre será necesario adaptar este flujo a tu infraestructura específica.
Por qué CI/CD para Umbraco On-Premises sigue siendo importante
Muchos equipos continúan alojando Umbraco en su propia infraestructura — a veces por motivos de compliance, otras por integración con sistemas locales y, en otros casos, porque la migración a la nube simplemente no es viable.
Sin embargo, los desarrolladores esperan automatización al estilo cloud, independientemente de dónde se encuentren los servidores.
Los pipelines de CI/CD ofrecen:
- Despliegues predecibles
- Rollouts seguros
- Eliminación de la copia manual de archivos
- Configuración consistente del servidor
- Automatización amigable para el developer
Arquitectura de Alto Nivel
Azure DevOps (Cloud)
- Repositorio Git de tu solución Umbraco
- Pipeline YAML multi-stage
- Azure DevOps Environments (UAT & PROD)
Servidores On-Premises
- Windows Server con IIS
- Agentes self-hosted de Azure DevOps
- Carpetas wwwroot dedicadas para UAT & PROD
Flujo de Deployment
- El developer hace push a
devorelease/uat El pipeline compila la aplicación (.NET + npm)- El output se despliega en UAT
- El mismo artefacto se promociona a Producción
Instalar o Agente Self-Hosted do Azure DevOps
1. Crear Agent Pool
Organization Settings → Agent Pools → New → Self-hosted
2. Descargar el Agente
New Agent → Windows → Download ZIP
3. Configurar el Agente
mkdir C:\azdo-agent cd C:\azdo-agent
Extraer el ZIP y ejecutar:
.\config.cmd
Después:
- URL del servidor: https://dev.azure.com/YOURORG/
- PAT: debe tener permisos Agent Pools (read, manage)
- Pool: OnPrem-IIS
- Nombre: UAT-IIS o PROD-IIS
- Ejecutar como servicio: Sí
Iniciar el agente:
.\run.cmd
Configurar Azure DevOps Environments
- Pipelines → Environments
- Crear UAT-IIS y registrar el servidor UAT
- Crear PROD-IIS y registrar el servidor PROD
Pipeline YAML Completo de CI/CD
Pipeline totalmente funcional — puedes copiar y pegar tal cual.
trigger:
- dev
- release/uat
variables:
buildConfiguration: 'Release'
projectFolder: 'www'
publishFolder: '$(Build.ArtifactStagingDirectory)/publish'
nodeVersion: '20.x'
offlineTemplate: 'App_Offline.htm.rename'
offlineFile: 'App_Offline.htm'
stages:
- stage: Build
displayName: 'Build & Publish Umbraco'
jobs:
- job: BuildJob
displayName: 'Build .NET + Tailwind + Vite'
pool:
vmImage: 'windows-latest'
steps:
- checkout: self
- task: NodeTool@0
displayName: 'Install Node.js'
inputs:
versionSpec: '$(nodeVersion)'
- task: Cache@2
displayName: 'Cache npm (frontend)'
inputs:
key: 'npm-frontend | "$(Build.SourcesDirectory)/$(projectFolder)/package-lock.json"'
path: '$(Build.SourcesDirectory)/$(projectFolder)/node_modules'
- task: Cache@2
displayName: 'Cache npm (backoffice)'
inputs:
key: 'npm-backend | "$(Build.SourcesDirectory)/dbl.MadeirasLeiria.backoffice/package-lock.json"'
path: '$(Build.SourcesDirectory)/dbl.MadeirasLeiria.backoffice/node_modules'
- task: Npm@1
displayName: 'npm install (backoffice)'
inputs:
command: 'install'
workingDir: 'dbl.MadeirasLeiria.backoffice'
- task: UseDotNet@2
displayName: 'Install .NET 9 SDK'
inputs:
packageType: 'sdk'
version: '9.0.x'
- task: DotNetCoreCLI@2
displayName: 'Restore NuGet'
inputs:
command: 'restore'
projects: '$(projectFolder)/www.csproj'
- task: DotNetCoreCLI@2
displayName: 'Publish'
inputs:
command: 'publish'
projects: '$(projectFolder)/www.csproj'
arguments: '--configuration $(buildConfiguration) --output $(publishFolder)'
zipAfterPublish: false
- publish: $(publishFolder)
artifact: drop
- stage: Deploy_UAT
displayName: 'Deploy to UAT'
dependsOn: Build
condition: succeeded()
jobs:
- deployment: UATDeploy
displayName: 'Deploy to UAT`
environment:
name: 'UAT-DBL01'
resourceType: VirtualMachine
strategy:
runOnce:
deploy:
steps:
- checkout: none
- download: current
artifact: drop
- task: PowerShell@2
displayName: 'Activate App_Offline'
inputs:
targetType: 'inline'
script: |
$root="D:\WEB\MadeiriasLeiria\uat.madeirasleiria.pt\wwwroot"
$template=Join-Path $root "$(offlineTemplate)"
$offline=Join-Path $root "$(offlineFile)"
if(Test-Path $template){
if(Test-Path $offline){ Remove-Item $offline -Force }
Rename-Item $template $offline -Force
}
- task: PowerShell@2
displayName: 'Clean UAT (preserve media + logs)'
inputs:
targetType: 'inline'
script: |
$path="D:\WEB\MadeiriasLeiria\uat.madeirasleiria.pt\wwwroot"
$preserve=@("wwwroot\media","umbraco\Logs","App_Offline.htm")
$temp=Join-Path $env:TEMP "backup_$(Get-Date -Format yyyyMMddHHmmss)"
New-Item $temp -ItemType Directory | Out-Null
foreach($item in $preserve){
$src=Join-Path $path $item
if(Test-Path $src){
$dest=Join-Path $temp $item
New-Item (Split-Path $dest) -ItemType Directory -Force | Out-Null
robocopy $src $dest /E | Out-Null
}
}
Get-ChildItem $path | ForEach-Object {
if($_.Name -ne "App_Offline.htm"){
Remove-Item $_.FullName -Recurse -Force
}
}
foreach($item in $preserve){
$src=Join-Path $temp $item
if(Test-Path $src){
$dest=Join-Path $path $item
New-Item (Split-Path $dest) -ItemType Directory -Force | Out-Null
robocopy $src $dest /E | Out-Null
}
}
Remove-Item $temp -Recurse -Force
- task: CopyFiles@2
displayName: 'Copy Files'
inputs:
SourceFolder: '$(Pipeline.Workspace)/drop/www'
Contents: |
**
!wwwroot/media/**
!umbraco/Logs/**
TargetFolder: 'D:\WEB\MadeiriasLeiria\uat.madeirasleiria.pt\wwwroot'
OverWrite: true
- task: PowerShell@2
displayName: 'Set ASPNETCORE_ENVIRONMENT'
inputs:
targetType: 'inline'
script: |
$config="D:\WEB\MadeiriasLeiria\uat.madeirasleiria.pt\wwwroot\web.config"
[xml]$xml = Get-Content $config
$node=$xml.SelectSingleNode("//environmentVariable[@name='ASPNETCORE_ENVIRONMENT']")
if($node -eq $null){
$envNode=$xml.SelectSingleNode("//environmentVariables")
$newNode=$xml.CreateElement("environmentVariable")
$newNode.SetAttribute("name","ASPNETCORE_ENVIRONMENT")
$newNode.SetAttribute("value","Staging")
$envNode.AppendChild($newNode)
} else {
$node.value="Staging"
}
$xml.Save($config)
- task: PowerShell@2
displayName: 'Set ACLs'
inputs:
targetType: 'inline'
script: |
$root="D:\WEB\MadeiriasLeiria\uat.madeirasleiria.pt\wwwroot"
$appPool="IIS AppPool\uat.madeirasleiria.pt"
$dirs=@("umbraco","App_Plugins","Views","wwwroot\media","wwwroot\css","wwwroot\scripts","wwwroot\umbraco")
foreach($d in $dirs){
$full=Join-Path $root $d
if(Test-Path $full){
icacls $full /grant "$appPool:(OI)(CI)(M)" /T
}
}
- task: PowerShell@2
displayName: 'Deactivate App_Offline'
inputs:
targetType: 'inline'
script: |
$root="D:\WEB\MadeiriasLeiria\uat.madeirasleiria.pt\wwwroot"
$template=Join-Path $root "$(offlineTemplate)"
$offline=Join-Path $root "$(offlineFile)"
if(Test-Path $offline){
if(Test-Path $template){ Remove-Item $template -Force }
Rename-Item $offline $template -Force
}
- stage: Deploy_PROD
displayName: 'Deploy to Production'
dependsOn: Deploy_UAT
condition: succeeded()
jobs:
- deployment: ProdDeploy
environment:
name: 'PROD-DBL01'
resourceType: VirtualMachine
strategy:
runOnce:
deploy:
steps:
- checkout: none
- download: current
artifact: drop
# SAME LOGIC AS UAT, WITH PRODUCTION PATHS
¿Necesitas ayuda con tus despliegues de Umbraco?
Si estás trabajando en una nueva configuración de CI/CD o intentando mejorar la existente, y todo empieza a complicarse cuando añades múltiples servidores, sincronización de media, reglas de seguridad o flujos más avanzados… no estás solo. Ayudamos a equipos con esto todos los días.
Nuestros developers han construido y desplegado proyectos Umbraco de todos los tamaños: sitios para pequeñas empresas, grandes plataformas empresariales, proyectos del sector público y todo lo que hay entre estos extremos.
Si deseas que alguien con experiencia revise tu configuración o simplemente necesitas orientación sobre el mejor enfoque, estaremos encantados de ayudarte.
Habla con nuestro equipo
Si quieres revisar tu proceso de deployment o planificar los próximos pasos, ponte en contacto con nosotros.
Sin presión ni compromisos. Solo una conversación cercana sobre lo que deseas conseguir.
Si te enfrentas a desafíos relacionados con balanceo de carga, escalabilidad, automatización de despliegues o cualquier tema vinculado a Umbraco y DevOps, habla con nosotros. Podemos ser el equipo adecuado para apoyarte (y nuestros clientes suelen considerar nuestros precios muy competitivos).
Escrito por Marco Teodoro
Fundador & CEO, Double

Conecta-te à minha rede no LinkedIn | Segue a Double no LinkedIn
#Umbraco #AgenciaNearshore #MigracaoUmbraco