DevOps DevOps Deployment .NET Shared Hosting CI/CD

Deploying .NET Core to Shared Hosting (No CLI)

Publish profiles, web.config tricks, and EF Core migration automation on budget hosting.

Q

Quantums Team

April 09, 2026

10 min read

The Reality of Budget Hosting

Not every project has a $500/month Azure budget. Plenty of real-world .NET Core apps run perfectly well on shared hosting that costs $5–15/month — cPanel-based hosts, Plesk panels, or basic VPS instances where you don't have SSH access (or your client doesn't want you touching the server directly).

The .NET ecosystem is heavily optimised for cloud-native deployment with CLI pipelines. Getting it to work on traditional hosting requires a few specific techniques that aren't obvious from the documentation.

Step 1: Publish Profiles

Create a publish profile targeting the host's file system layout. In Visual Studio or Rider, create a new Publish Profile with "Folder" target:

<!-- Properties/PublishProfiles/shared-host.pubxml -->
<Project>
  <PropertyGroup>
    <PublishDir>bin\publish</PublishDir>
    <PublishProtocol>FileSystem</PublishProtocol>
    <Configuration>Release</Configuration>
    <TargetFramework>net8.0</TargetFramework>
    <SelfContained>false</SelfContained>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
  </PropertyGroup>
</Project>

Run dotnet publish -p:PublishProfile=shared-host and you get a folder ready to FTP. If the host runs Linux (common on cPanel with ASP.NET Core module), change RuntimeIdentifier to linux-x64.

Step 2: web.config for IIS/Plesk

The .NET Core module for IIS handles the process lifecycle. Your web.config should look like this (it's auto-generated by publish, but good to understand):

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*"
             modules="AspNetCoreModuleV2"
             resourceType="Unspecified"/>
      </handlers>
      <aspNetCore processPath="dotnet"
                  arguments=".\QuantumsWeb.dll"
                  stdoutLogEnabled="false"
                  stdoutLogFile=".\logs\stdout"
                  hostingModel="inprocess">
        <environmentVariables>
          <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production" />
        </environmentVariables>
      </aspNetCore>
    </system.webServer>
  </location>
</configuration>

Step 3: Secrets Without Environment Variables

On shared hosting you often can't set real environment variables. Use appsettings.Production.json (not committed to source control) or, better, put secrets in a web.config <environmentVariables> block — it's IIS-managed and not served publicly:

<environmentVariables>
  <environmentVariable name="ConnectionStrings__Default" value="Server=..." />
  <environmentVariable name="Jwt__Secret" value="your-secret-here" />
</environmentVariables>

Step 4: Database Migrations Without EF CLI

You can't run dotnet ef database update from FTP. The cleanest solution is to apply migrations at startup:

// Program.cs — apply pending migrations automatically at boot
using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    await db.Database.MigrateAsync(); // applies pending migrations
    await ApplicationDbContext.SeedAsync(db);
}

If you're using the manual SQL schema approach (like Quantums does — CREATE TABLE IF NOT EXISTS scripts), the same pattern works: just call your schema bootstrap method instead of MigrateAsync().

Step 5: Handling App Restarts

Shared hosts recycle the app pool frequently — sometimes on every request after idle timeout. Make sure your app boots fast. Keep Program.cs lean, avoid expensive startup work, and make database connections lazy (EF Core does this by default).

To trigger an app pool recycle after deploying via FTP, create or touch a file called app_offline.htm in the web root, do your FTP upload, then delete app_offline.htm. IIS will gracefully drain existing requests, then restart the process with your new code.

Deploying to cPanel (Linux + Kestrel Proxy)

Some modern cPanel hosts run .NET Core via Kestrel behind Apache with mod_proxy. The process is slightly different:

  1. Upload your publish output to a non-public folder (e.g., ~/apps/myapp/)
  2. Set up a Node.js/Python app entry in cPanel (some hosts use this as a proxy mechanism) or configure Apache vhost with ProxyPass / to your Kestrel port
  3. Create a startup script that runs dotnet QuantumsWeb.dll --urls http://127.0.0.1:5000
  4. Use a .htaccess ProxyPass rule to forward traffic

It's more complex but it works — we run three production sites this way, total hosting cost under $30/month.