CI/CD Moderno para Umbraco com Azure DevOps e IIS On-Premises


25 nov, 2025 09:04

Partilhe esta notícia:

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

  1. O developer faz push para dev ou release/uat
  2. O pipeline compila a app (.NET + npm)
  3. O output é deployado para UAT
  4. 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

  1. Pipelines → Environments
  2. Criar UAT-IIS e registar o servidor UAT
  3. 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

 


comments powered by Disqus