CI/CD Moderno para Umbraco Usando Azure DevOps & IIS On-Premises (Atualização 2025)
Há alguns anos, partilhei um guia passo-a-passo sobre como fazer deploy de sites Umbraco para servidores on-premises usando os clássicos Release Pipelines do Azure DevOps. Muito mudou desde então. Hoje, o Umbraco corre em .NET moderno, a maioria dos projetos usa Vite ou Tailwind para tooling frontend, e o Azure DevOps incentiva fortemente pipelines em YAML em vez dos antigos releases configurados por interface.
Este guia atualizado reflete como construímos e entregamos websites Umbraco em 2025 — usando automação total, deployments sensíveis ao ambiente e um único pipeline YAML que faz deploy para UAT e Produção em servidores IIS dentro do teu próprio datacenter.
Se estás a modernizar um processo antigo ou a começar do zero, este artigo dá-te uma base sólida. Mas lembra-te: isto é um boilerplate. Cada organização tem as suas necessidades — load balancing, replicação, estratégias de media partilhados, gestão de segredos, reforço de segurança e governance de deploy. Quase sempre será necessário adaptar este fluxo para corresponder à tua infraestrutura específica.
Porque é que CI/CD para Umbraco On-Premises Continua a Ser Importante
Muitas equipas continuam a alojar Umbraco na sua própria infraestrutura — às vezes por motivos de compliance, outras vezes pela integração com sistemas locais, e outras porque a migração para a cloud simplesmente não é prática. Os developers, no entanto, esperam automação ao estilo cloud independentemente de onde os servidores estão.
Pipelines de CI/CD oferecem:
- Deployments previsíveis
- Rollouts seguros
- Eliminação de cópia manual de ficheiros
- Configuração consistente do servidor
- Automação amiga do developer
Arquitetura de Alto Nível
Azure DevOps (Cloud)
- Repositório Git da tua solução Umbraco
- Pipeline YAML multi-stage
- Azure DevOps Environments (UAT & PROD)
Servidores On-Premises
- Windows Server com IIS
- Agentes self-hosted Azure DevOps
- Pastas wwwroot dedicadas para UAT & PROD
Fluxo de Deployment
- O developer faz push para dev ou release/uat
- O pipeline compila a app (.NET + npm)
- O output é deployado para UAT
- O mesmo artefacto é promovido para Produção
Instalar o Agente Self-Hosted do Azure DevOps
1. Criar Agent Pool
Organization Settings → Agent Pools → New → Self-hosted
2. Fazer Download do Agente
New Agent → Windows → Download ZIP
3. Configurar o Agente
mkdir C:\azdo-agent cd C:\azdo-agent
Extrair o ZIP e correr:
.\config.cmd
Depois:
- URL do servidor: https://dev.azure.com/YOURORG/
- PAT: deve ter permissões Agent Pools (read, manage)
- Pool: OnPrem-IIS
- Nome: UAT-IIS ou PROD-IIS
- Correr como serviço: Sim
Iniciar o agente:
.\run.cmd
Configurar Azure DevOps Environments
- Pipelines → Environments
- Criar UAT-IIS e registar o servidor UAT
- Criar PROD-IIS e registar o servidor PROD
Pipeline YAML Completo de CI/CD
Pipeline totalmente funcional — podes copiar e colar tal como está.
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
Precisa de Ajuda com os seus Deploys Umbraco?
Se está a trabalhar numa nova configuração de CI/CD ou a tentar melhorar a que já tem, e tudo começa a complicar-se quando adiciona múltiplos servidores, sincronização de media, regras de segurança ou fluxos mais avançados... não está sozinho. Ajudamos equipas com isto todos os dias.
Os nossos developers construíram e fizeram deploy de projetos Umbraco de todas as dimensões: sites para pequenas empresas, grandes plataformas empresariais, projetos do setor público e tudo o que há entre estes extremos. Se quiser que alguém experiente analise a sua configuração, ou se quer apenas aconselhamento sobre a melhor abordagem, teremos todo o gosto em ajudar.
Fale com a nossa equipa
Se quiser rever o seu processo de deployment ou planear os próximos passos, entre em contacto connosco. Sem pressão ou compromissos. Apenas uma conversa amigável sobre o que deseja alcançar.
Contacte-nos
Se está a enfrentar desafios com load balancing, escalabilidade, automação de deploy ou qualquer tema relacionado com Umbraco e DevOps, fale connosco. Podemos ser a equipa certa para o apoiar (e os nossos clientes costumam considerar os nossos preços muito competitivos).
Escrito por Marco Teodoro
Fundador & CEO, Double

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