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 invokesmsdeploy.exefrom the Windows agent.- Secrets (credentials, URLs) are stored as secret pipeline variables, never in plain text in the YAML.
-skiprules 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 yourSERVER_COMPUTER_NAMEin the SmarterASP.NET control panel → Publish your website → Connection tab. It always starts withhttps://winXXXX.site4now.net:8172.
Step by step: getting it up and running
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.
Create the
azure-pipelines.ymlfile at the root of your Git repository. Copy the YAML above and adaptProjectPathandDotNetVersionto your project.In Azure DevOps, go to Pipelines → New pipeline, point to your repository, and select Existing Azure Pipelines YAML file.
Add the variables in the Azure DevOps interface (not in the YAML!). Check Keep this value secret for the password and username.
Create the
prodenvironment under Pipelines → Environments. You can add a manual approval gate before deployment if you work in a team.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 versionedappsettings.json. Useappsettings.Production.jsonoutside 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 tomainby adding a trigger at the top of the YAML:trigger: - mainDevelopment 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.


Commentaires (0)
Aucun commentaire pour le moment. Soyez le premier à commenter !
Laisser un commentaire