Retour aux articles

Deploying a .NET Web App on SmarterASP with Azure DevOps (Step-by-Step CI/CD)

Deploying a .NET Web App on SmarterASP with Azure DevOps (Step-by-Step CI/CD)

You're hosting an ASP.NET Core or Blazor app on SmarterASP.NET and you're tired of copying files manually via FileZilla? This guide walks you through setting up a complete CI/CD pipeline with Azure DevOps that automatically builds, publishes, and deploys your application on every push.

TL;DR

  • SmarterASP.NET exposes a WebDeploy (MSDeploy) endpoint on port 8172.
  • Azure DevOps builds your project with dotnet publish, publishes an artifact, then invokes msdeploy.exe from the Windows agent.
  • Secrets (credentials, URLs) are stored as secret pipeline variables, never in plain text in the YAML.
  • -skip rules protect critical server-side files (sitemaps, Let's Encrypt certificates…).

Why WebDeploy instead of FTP?

FTP is fine for a first deployment at 2am. But for a project that evolves — multiple developers, branches, environments — it becomes a source of silent bugs: a forgotten file, an old DLL left behind, a partial deployment after a timeout. WebDeploy (aka MSDeploy) solves this by performing a differential sync between your publish folder and the server. It only transfers what has changed, and can remove obsolete files.

SmarterASP.NET supports WebDeploy natively. Their endpoint looks like https://winXXXX.site4now.net:8172/msdeploy.axd. We'll use it directly from Azure DevOps.

ℹ Prerequisites
You need an Azure DevOps account (free for small projects), a Git repository, and your SmarterASP.NET credentials (username / password from your control panel). WebDeploy must be enabled in your site settings.
Not a customer yet? My partner link gives you access to a promotional offer.


Pipeline anatomy

The pipeline is split into two stages: Build and Deploy. This separation matters: it lets you add a test stage in between, or deploy the same artifact to multiple environments.

stages:
  - stage: Build          # dotnet restore → build → publish → artifact
  - stage: Deploy         # download artifact → msdeploy → warm-up
    dependsOn: Build
    condition: succeeded()

The full annotated pipeline

pool:
  vmImage: 'windows-latest'   # MSDeploy only exists on Windows

variables:
  BuildConfiguration: 'Release'
  DotNetVersion: '10.x'       # Centralized here to simplify future upgrades
  ProjectPath: 'src\{path-to-your-csproj}.csproj'
  PreserveRemoteFiles: 'false' # true = do not delete unknown files on the server

# -------------------------------------------------------
# STAGE 1 - Build
# -------------------------------------------------------
stages:
- stage: Build
  displayName: 'Build & Publish'
  jobs:
  - job: Build
    steps:
    - task: UseDotNet@2
      displayName: 'Install .NET SDK $(DotNetVersion)'
      inputs:
        packageType: 'sdk'
        version: '$(DotNetVersion)'

    - task: DotNetCoreCLI@2
      displayName: 'Restore'
      inputs:
        command: 'restore'
        projects: '$(ProjectPath)'

    - task: DotNetCoreCLI@2
      displayName: 'Build'
      inputs:
        command: 'build'
        projects: '$(ProjectPath)'
        arguments: '--configuration $(BuildConfiguration) --no-restore'

    - task: DotNetCoreCLI@2
      displayName: 'Publish'
      inputs:
        command: 'publish'
        publishWebProjects: false
        projects: '$(ProjectPath)'
        # --no-build avoids recompiling what was just built
        arguments: >-
          --configuration $(BuildConfiguration)
          --output $(Build.ArtifactStagingDirectory)
          --no-build
        zipAfterPublish: false

    # Publish the generated folder as an artifact named "webapp"
    - publish: '$(Build.ArtifactStagingDirectory)/webapp'
      displayName: 'Publish webapp artifact'
      artifact: 'webapp'

# -------------------------------------------------------
# STAGE 2 - Deploy
# -------------------------------------------------------
- stage: Deploy
  displayName: 'WebDeploy to SmarterASP.NET'
  dependsOn: Build
  condition: succeeded()
  jobs:
  - deployment: DeployToSmarterASP
    environment: prod
    strategy:
      runOnce:
        deploy:
          steps:
          - download: current
            artifact: 'webapp'

          - powershell: |
              $sourcePath = "$(Pipeline.Workspace)\webapp"

              # Look for msdeploy at the two standard locations on Microsoft-hosted agents
              $msdeploy = @(
                "C:\Program Files\IIS\Microsoft Web Deploy V3\msdeploy.exe",
                "C:\Program Files (x86)\IIS\Microsoft Web Deploy V3\msdeploy.exe"
              ) | Where-Object { Test-Path $_ } | Select-Object -First 1

              if (-not $msdeploy) { throw "msdeploy.exe not found on this agent" }
              Write-Host "msdeploy found: $msdeploy"

              $msdeployArgs = @(
                '-verb:sync',
                "-source:contentPath=""$sourcePath""",
                "-dest:contentPath=""$(WEBSITE_NAME)"",computerName=""$(SERVER_COMPUTER_NAME)"",userName=""$(SERVER_USERNAME)"",password=""$(SERVER_PASSWORD)"",authtype=""Basic"",includeAcls=""False""",
                '-allowUntrusted',
                '-disableLink:AppPoolExtension',
                '-disableLink:ContentExtension',
                '-disableLink:CertificateExtension',
                '-enableRule:AppOffline'   # Places an app_offline.htm during deployment
              )

              # Option: do not delete unknown files on the remote server
              if ("$(PreserveRemoteFiles)".ToLower() -eq "true") {
                $msdeployArgs += '-enableRule:DoNotDeleteRule'
              }

              # --- Custom exclusions (via PARAM_EXCLUDED_FILES variable) ---
              "$(PARAM_EXCLUDED_FILES)".Split(";") |
                Where-Object { $_.Trim() -ne "" } |
                ForEach-Object {
                  $regex = "(?i).*$([Regex]::Escape($_.Trim()))$"
                  Write-Host "Excluding file: $regex"
                  $msdeployArgs += "-skip:skipAction=Delete,objectName=filePath,absolutePath=`"$regex`""
                }

              # --- Protect critical server-side folders ---

              # Let's Encrypt certificates (ACME challenge)
              $msdeployArgs += "-skip:skipAction=Delete,objectName=dirPath,absolutePath=`"(?i).*\\\.well-known.*`""

              # Dynamically generated sitemaps
              $msdeployArgs += "-skip:skipAction=Delete,objectName=dirPath,absolutePath=`"(?i).*\\wwwroot\\sitemaps(`$|\\.*)`""
              $msdeployArgs += "-skip:skipAction=Delete,objectName=filePath,absolutePath=`"(?i).*\\wwwroot\\sitemaps\\.*`""

              Write-Host "Running msdeploy..."
              & $msdeploy @msdeployArgs

              if ($LASTEXITCODE -ne 0) {
                Write-Error "msdeploy failed with exit code $LASTEXITCODE"
                exit $LASTEXITCODE
              }
            displayName: 'WebDeploy to SmarterASP.NET'
            env:
              WEBSITE_NAME:          '$(WEBSITE_NAME)'
              SERVER_COMPUTER_NAME:  '$(SERVER_COMPUTER_NAME)'
              SERVER_USERNAME:       '$(SERVER_USERNAME)'
              SERVER_PASSWORD:       '$(SERVER_PASSWORD)'

          # Warm-up: wake the app up after deployment (ASP.NET Core starts lazily)
          - powershell: |
              $url = "$(WEBSITE_URL)"
              $maxRetries = 3
              $retryDelay = 10
              Write-Host "Warming up $url..."
              for ($i = 1; $i -le $maxRetries; $i++) {
                try {
                  $r = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 45
                  Write-Host "Site responded: $($r.StatusCode) - OK"
                  break
                } catch {
                  Write-Host "Attempt $i/$maxRetries failed: $_"
                  if ($i -lt $maxRetries) { Start-Sleep -Seconds $retryDelay }
                }
              }
            displayName: 'Application warm-up'
            continueOnError: true   # A failed warm-up should not fail the deployment

Variables to configure in Azure DevOps

Go to Pipelines → your pipeline → Edit → Variables. Some are public, others must be marked as secret so they never appear in the logs.

Variable Example value Secret?
WEBSITE_NAME mysite.com No
SERVER_COMPUTER_NAME https://win1234.site4now.net:8172/msdeploy.axd?site=mysite.com No
SERVER_USERNAME your-smarterasp-login Yes
SERVER_PASSWORD ••••••••• Yes
WEBSITE_URL https://mysite.com No
PARAM_EXCLUDED_FILES appsettings.Production.json;Web.config No

✓ Tip
Find your SERVER_COMPUTER_NAME in the SmarterASP.NET control panel → Publish your websiteConnection tab. It always starts with https://winXXXX.site4now.net:8172.


Step by step: getting it up and running

  1. Enable WebDeploy in the SmarterASP.NET control panel. It is enabled by default, but verify that port 8172 is not blocked by a firewall on your end.

  2. Create the azure-pipelines.yml file at the root of your Git repository. Copy the YAML above and adapt ProjectPath and DotNetVersion to your project.

  3. In Azure DevOps, go to Pipelines → New pipeline, point to your repository, and select Existing Azure Pipelines YAML file.

  4. Add the variables in the Azure DevOps interface (not in the YAML!). Check Keep this value secret for the password and username.

  5. Create the prod environment under Pipelines → Environments. You can add a manual approval gate before deployment if you work in a team.

  6. Trigger a first run and watch the logs. If msdeploy fails, the error message is usually explicit (wrong credentials, closed port, untrusted certificate).


Common pitfalls

"msdeploy not found"

Microsoft-hosted windows-latest agents include Web Deploy V3. The script checks both standard locations. If it still fails, add a step to install WebDeploy via Chocolatey:

choco install webdeploy -y

Untrusted SSL certificate on the agent

The -allowUntrusted flag in the msdeploy command handles this. SmarterASP.NET uses valid certificates, but hosted agents can occasionally have trust chain issues.

My appsettings.Production.json gets overwritten

This is the number one production issue. Add this file to the PARAM_EXCLUDED_FILES variable: the PowerShell script automatically converts it into a -skip rule that prevents msdeploy from deleting or replacing it.

⚠ Warning
Never put production secrets (ConnectionStrings, API keys…) in a versioned appsettings.json. Use appsettings.Production.json outside your Git repository, or better yet: Azure Key Vault if your SmarterASP.NET plan allows it.

The warm-up fails with a 302

That's normal. If your site redirects HTTP → HTTPS, Invoke-WebRequest may report an error on the redirect. The continueOnError: true flag means the pipeline does not treat this as a blocking failure. The site runs perfectly fine regardless.

My dynamically generated sitemaps disappear

If your Blazor app writes sitemaps to wwwroot/sitemaps/ at runtime, those files don't exist in the publish artifact. Without protection, msdeploy deletes them on every deployment. The two dedicated -skip rules in the script protect that folder.


Going further

Add tests before deployment

Insert a Test job between Build and Deploy, or add a DotNetCoreCLI@2 step with command: 'test' in the Build stage. Azure DevOps integrates test results directly into its interface.

Deploy to multiple environments

Duplicate the Deploy stage and name them staging and prod. Use separate variable groups for each environment (Library → Variable groups). Add a manual approval gate on the prod environment under Pipelines → Environments.

Get notified on failure

Azure DevOps can send emails or Teams/Slack messages when a pipeline fails. Configure this under Project Settings → Notifications, or via an InvokeRestAPI task that calls a webhook.

✓ Final best practice
Trigger the pipeline only on commits to main by adding a trigger at the top of the YAML:

trigger:
  - main

Development branches can then be pushed freely without triggering a production deployment.


Conclusion

Setting up this pipeline takes about an hour the first time, but that time is paid back on every subsequent deployment. No more FileZilla, no more risk of forgetting a file, no more late-night deployments with fingers crossed. Just a git push to main and your app is live two minutes later.

The Azure DevOps + MSDeploy + SmarterASP.NET combination is less well-known than the major managed cloud platforms, but it works great for .NET projects that don't need — or can't afford — Azure App Service or AWS Elastic Beanstalk. This guide should get you there without the usual trial and error.

If you have questions about a specific topic — EF Core migrations at deploy time, multi-environment setups, or Azure Key Vault integration — drop them in the comments.

ASP.NET Hosting - SmarterASP.NET

Commentaires (0)

Aucun commentaire pour le moment. Soyez le premier à commenter !

Laisser un commentaire

Votre email ne sera pas publié.

© 2025 Pierre-Henri

Tous droits réservés.

Projects
Privacy Policy

Développé avec et .NET

An unhandled error has occurred. Reload 🗙

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please reload the page.