A single publish folder with production connection strings checked into CI is how staging quietly points at live SQL. On Windows shared hosting and VPS boxes you rarely get Azure-style deployment slots, so environment differences have to land at deploy time—not build time. Web Deploy parameters plus a tight publish profile give you that split without rebuilding the app for every host.
The practical pattern: build once for win-x64 (or portable), ship the same artifact to each IIS site, and inject connection strings, app settings, and environment name through MSDeploy parameters or a SetParameters file. That keeps secrets out of the package and keeps staging from sharing production SQL pools.
#One package, many IIS targets
ASP.NET Core 10 on IIS still publishes a folder: DLLs, web.config, wwwroot, and deps. Environment-specific values should not be compiled into that folder if you promote the same bits. Prefer IConfiguration sources that IIS and the host can override—environment variables, web.config env vars, and parameterized appSettings—over hard-coded appsettings.Production.json inside the zip.
On a VPS you can keep two sites (app-staging and app-www) under separate app pools and physical paths. On shared Windows hosting you often get one site plus a subdomain or a second application under the same pool limits. Either way, parameterization beats “edit web.config on the server after every push.”
#Publish profiles that match real IIS paths
A .pubxml profile should name the Web Deploy endpoint, site path, and auth method you actually use—not a leftover Azure profile. Keep profiles out of the repo if they hold passwords; store credentials in CI secrets and pass them on the msdeploy or dotnet publish command line.
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<WebPublishMethod>MSDeploy</WebPublishMethod>
<MSDeployServiceURL>https://deploy.example.com:8172/msdeploy.axd</MSDeployServiceURL>
<DeployIisAppPath>staging.example.com</DeployIisAppPath>
<EnableMSDeployAppOffline>true</EnableMSDeployAppOffline>
<AllowUntrustedCertificate>false</AllowUntrustedCertificate>
<SkipExtraFilesOnServer>true</SkipExtraFilesOnServer>
</PropertyGroup>
</Project>
EnableMSDeployAppOffline drops app_offline.htm during sync so in-flight requests fail fast instead of loading half-written assemblies. SkipExtraFilesOnServer protects uploaded content and logs you did not publish. Pair that with an app pool identity that can write only the folders it must (App_Data, logs), not the whole site root.
#Parameterize connection strings and settings
Declare parameters in Parameters.xml (or let the publish targets generate them) and supply values per environment in CI. For classic web.config connectionStrings and appSettings entries, MSDeploy can replace values during sync. For ASP.NET Core, prefer setting ASPNETCORE_ENVIRONMENT and overriding configuration via environment variables or a transformed web.config aspNetCore element—still driven by the same parameter pass.
<?xml version="1.0" encoding="utf-8"?>
<parameters>
<parameter name="Environment"
description="ASPNETCORE_ENVIRONMENT"
defaultValue="Staging">
<parameterEntry kind="XmlFile"
scope="\\web\.config$"
match="/configuration/location/system.webServer/aspNetCore/environmentVariables/environmentVariable[@name='ASPNETCORE_ENVIRONMENT']/@value" />
</parameter>
<parameter name="SqlConnection"
description="Primary SQL connection"
defaultValue="">
<parameterEntry kind="XmlFile"
scope="\\appsettings\..*\.json$"
match="$.ConnectionStrings.DefaultConnection" />
</parameter>
</parameters>
JSON path matching for appsettings is convenient but brittle if the file is minified differently per publish. A more durable approach on IIS is: keep non-secret defaults in appsettings.json, set ASPNETCORE_ENVIRONMENT via the parameterized web.config, and inject secrets as environment variables on the aspNetCore element or at the app pool level on a VPS. Shared hosts that expose Web Deploy parameters but not pool env UI still work well with web.config variable entries.
# CI example: same artifact, different parameter values
$pkg = ".\publish\App.zip"
$dest = "https://deploy.example.com:8172/msdeploy.axd"
$site = "staging.example.com"
msdeploy.exe -verb:sync `
-source:package=$pkg `
-dest:auto,computerName=$dest,site=$site,authType=Basic,userName=$env:WD_USER,password=$env:WD_PASS `
-setParam:name="Environment",value="Staging" `
-setParam:name="SqlConnection",value="$env:STAGING_SQL" `
-enableRule:AppOffline `
-allowUntrusted:$false
#After sync: prove the right environment answered
Parameterization only helps if you verify what landed. Hit a dedicated health endpoint that returns environment name (not secrets), assembly informational version, and a simple DB ping. Run it from CI against the staging host name before you repeat the sync to production with production parameters.
- Confirm ASPNETCORE_ENVIRONMENT matches the parameter you sent (Staging vs Production).
- Confirm the SQL catalog name or Application Name in the connection string is the non-prod database.
- Confirm the site binding and host header you warmed are the ones customers use—not only localhost on the box.
- Keep app_offline.htm removed after a successful sync; a failed deploy that leaves it up is an outage.
On Windows Server 2025 with IIS 10.0, in-process ASP.NET Core hosting still reads the aspNetCore section at worker start. If you change only environment variables in web.config, recycle the app pool or rely on the AppOffline rule so the new values load before traffic returns.
#What to keep out of the package
Do not publish user secrets, local DB files, or machine-specific appsettings.*.json that embed passwords. Exclude them in the csproj so a laptop publish cannot leak into the zip. Use Web Deploy skip rules for App_Data\uploads if content is created at runtime and must survive syncs.
<ItemGroup>
<Content Update="appsettings.*.local.json" CopyToPublishDirectory="Never" />
<Content Update="**\*.db" CopyToPublishDirectory="Never" />
</ItemGroup>
Takeaway: treat Web Deploy parameters as your environment boundary on shared and VPS IIS. Build .NET 10 once, keep publish profiles honest about site paths and AppOffline, inject SQL and ASPNETCORE_ENVIRONMENT at sync time, and smoke-test the host header you actually serve. That is the closest thing to slots most Windows hosts give you—and it fails safer than hand-editing config on the box after every release.
Comments
No comments yet