Skip to main content

On-Premises Deployment

Framework: Excalibur.Dispatch Deployment Target: On-premises servers (Windows/Linux) Last Updated: 2026-01-01


Before You Start

Overview

Deploy Excalibur applications to on-premises infrastructure using IIS, Windows Services, or systemd for full control over hosting environment.

Use on-premises deployment when:

  • Regulatory requirements mandate data residency
  • Existing infrastructure investment
  • Air-gapped or offline environments
  • Full control over hardware and network configuration

IIS Deployment (Windows Server)

Prerequisites

  • Windows Server 2019 or later
  • IIS 10+ with ASP.NET Core Hosting Bundle
  • .NET 9 Runtime or SDK

Install ASP.NET Core Hosting Bundle

# Download and install
Invoke-WebRequest -Uri https://dot.net/v1/dotnet-install.ps1 -OutFile dotnet-install.ps1
.\dotnet-install.ps1 -Runtime aspnetcore -Version 9.0

# Verify installation
dotnet --list-runtimes

Publish Application

# Publish for IIS deployment
dotnet publish -c Release -o C:\inetpub\wwwroot\your-app

# Or with self-contained runtime
dotnet publish -c Release -r win-x64 --self-contained -o C:\inetpub\wwwroot\your-app

Create IIS Application Pool

# Import IIS module
Import-Module WebAdministration

# Create application pool
New-WebAppPool -Name "YourAppPool"

# Configure application pool
Set-ItemProperty -Path "IIS:\AppPools\YourAppPool" -Name "managedRuntimeVersion" -Value ""
Set-ItemProperty -Path "IIS:\AppPools\YourAppPool" -Name "processModel.identityType" -Value "ApplicationPoolIdentity"
Set-ItemProperty -Path "IIS:\AppPools\YourAppPool" -Name "enable32BitAppOnWin64" -Value $false

# Set recycling schedule (daily at 2 AM)
Clear-ItemProperty -Path "IIS:\AppPools\YourAppPool" -Name "recycling.periodicRestart.schedule"
New-ItemProperty -Path "IIS:\AppPools\YourAppPool" -Name "recycling.periodicRestart.schedule" -Value @{value="02:00:00"}

Create IIS Website

# Create website
New-Website -Name "YourApp" `
-PhysicalPath "C:\inetpub\wwwroot\your-app" `
-ApplicationPool "YourAppPool" `
-Port 80

# Configure HTTPS (recommended)
$cert = New-SelfSignedCertificate -DnsName "yourdomain.com" -CertStoreLocation "Cert:\LocalMachine\My"
New-WebBinding -Name "YourApp" -Protocol "https" -Port 443
$binding = Get-WebBinding -Name "YourApp" -Protocol "https"
$binding.AddSslCertificate($cert.Thumbprint, "My")

# Configure authentication
Set-WebConfigurationProperty -Filter "/system.webServer/security/authentication/anonymousAuthentication" `
-Name "enabled" -Value $true -PSPath "IIS:\Sites\YourApp"

# Start website
Start-Website -Name "YourApp"

web.config (Automatic)

ASP.NET Core generates web.config automatically during publish:

<?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=".\YourApp.dll"
stdoutLogEnabled="true"
stdoutLogFile=".\logs\stdout"
hostingModel="inprocess" />
</system.webServer>
</location>
</configuration>

Application Configuration

// appsettings.Production.json
{
"ConnectionStrings": {
"Default": "Server=.\\SQLEXPRESS;Database=AppDb;Integrated Security=true;TrustServerCertificate=true;"
},
"Dispatch": {
"Outbox": {
"ProcessorInterval": "00:00:30",
"BatchSize": 100
}
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

Troubleshooting IIS

# Check IIS logs
Get-Content "C:\inetpub\wwwroot\your-app\logs\stdout*.log" -Tail 50

# Check Event Viewer
Get-EventLog -LogName Application -Source "IIS AspNetCore Module V2" -Newest 20

# Restart application pool
Restart-WebAppPool -Name "YourAppPool"

# Test website
Test-NetConnection -ComputerName localhost -Port 80

Windows Service Deployment

Create Windows Service Project

// Program.cs
using Microsoft.Extensions.Hosting.WindowsServices;

var builder = Host.CreateApplicationBuilder(args);

// Configure as Windows Service
builder.Services.AddWindowsService(options =>
{
options.ServiceName = "Excalibur Dispatch Service";
});

// Add Dispatch
builder.Services.AddDispatch();
builder.Services.AddSqlServerOutboxStore(options =>
{
options.ConnectionString = builder.Configuration.GetConnectionString("Default");
});

// Add background service
builder.Services.AddOutboxHostedService();

var host = builder.Build();
await host.RunAsync();

Project File

<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.0" />
<PackageReference Include="Excalibur.Dispatch" Version="1.0.0" />
<PackageReference Include="Excalibur.Outbox.SqlServer" Version="1.0.0" />
</ItemGroup>
</Project>

Install Windows Service

# Publish as self-contained
dotnet publish -c Release -r win-x64 --self-contained -o C:\Services\YourApp

# Create service using sc.exe
sc.exe create "YourAppService" `
binPath= "C:\Services\YourApp\YourApp.exe" `
start= auto `
DisplayName= "Your Application Service"

# Or use New-Service (PowerShell 6+)
New-Service -Name "YourAppService" `
-BinaryPathName "C:\Services\YourApp\YourApp.exe" `
-DisplayName "Your Application Service" `
-StartupType Automatic `
-Description "Excalibur background service"

# Configure service recovery
sc.exe failure "YourAppService" reset= 86400 actions= restart/60000/restart/60000/restart/60000

# Start service
Start-Service -Name "YourAppService"

# Check status
Get-Service -Name "YourAppService"

Service Configuration (appsettings.json)

{
"ConnectionStrings": {
"Default": "Server=.\\SQLEXPRESS;Database=AppDb;Integrated Security=true;TrustServerCertificate=true;"
},
"Dispatch": {
"Outbox": {
"ProcessorInterval": "00:00:30",
"BatchSize": 100
}
},
"Logging": {
"LogLevel": {
"Default": "Information"
},
"EventLog": {
"SourceName": "YourApp",
"LogName": "Application"
}
}
}

Troubleshooting Windows Service

# Check service status
Get-Service -Name "YourAppService" | Format-List

# View Event Viewer logs
Get-EventLog -LogName Application -Source "YourApp" -Newest 20

# Stop and restart service
Stop-Service -Name "YourAppService"
Start-Service -Name "YourAppService"

# Uninstall service
Stop-Service -Name "YourAppService"
sc.exe delete "YourAppService"

systemd Deployment (Linux)

Publish for Linux

# Publish for Linux x64
dotnet publish -c Release -r linux-x64 --self-contained -o /opt/your-app

# Set permissions
sudo chown -R www-data:www-data /opt/your-app
sudo chmod +x /opt/your-app/YourApp

Create systemd Service Unit

# Create service file
sudo nano /etc/systemd/system/your-app.service

Service file content:

[Unit]
Description=Your Application Service
After=network.target

[Service]
Type=notify
User=www-data
Group=www-data
WorkingDirectory=/opt/your-app
ExecStart=/opt/your-app/YourApp
Restart=always
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=your-app
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/your-app/logs

[Install]
WantedBy=multi-user.target

Manage Service

# Reload systemd
sudo systemctl daemon-reload

# Enable service (start on boot)
sudo systemctl enable your-app.service

# Start service
sudo systemctl start your-app.service

# Check status
sudo systemctl status your-app.service

# View logs
sudo journalctl -u your-app.service -f

# Restart service
sudo systemctl restart your-app.service

# Stop service
sudo systemctl stop your-app.service

Reverse Proxy with Nginx

# Install Nginx
sudo apt update
sudo apt install nginx

# Configure Nginx
sudo nano /etc/nginx/sites-available/your-app

Nginx configuration:

server {
listen 80;
server_name yourdomain.com;

location / {
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

Enable site:

# Enable site
sudo ln -s /etc/nginx/sites-available/your-app /etc/nginx/sites-enabled/

# Test configuration
sudo nginx -t

# Reload Nginx
sudo systemctl reload nginx

SSL/TLS with Let's Encrypt

# Install Certbot
sudo apt install certbot python3-certbot-nginx

# Obtain certificate
sudo certbot --nginx -d yourdomain.com

# Auto-renewal (already configured by Certbot)
sudo certbot renew --dry-run

SQL Server Configuration

Windows Integrated Authentication

{
"ConnectionStrings": {
"Default": "Server=.\\SQLEXPRESS;Database=AppDb;Integrated Security=true;TrustServerCertificate=true;"
}
}

Grant permissions:

-- Create login for IIS app pool
CREATE LOGIN [IIS APPPOOL\YourAppPool] FROM WINDOWS;

-- Create user in database
USE AppDb;
CREATE USER [IIS APPPOOL\YourAppPool] FROM LOGIN [IIS APPPOOL\YourAppPool];

-- Grant permissions
ALTER ROLE db_datareader ADD MEMBER [IIS APPPOOL\YourAppPool];
ALTER ROLE db_datawriter ADD MEMBER [IIS APPPOOL\YourAppPool];
ALTER ROLE db_ddladmin ADD MEMBER [IIS APPPOOL\YourAppPool]; -- For migrations

SQL Server Authentication (Linux)

{
"ConnectionStrings": {
"Default": "Server=localhost;Database=AppDb;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=true;"
}
}

Secure password storage:

# Use environment variable
export ConnectionStrings__Default="Server=localhost;Database=AppDb;User Id=sa;Password=...;TrustServerCertificate=true;"

# Or use User Secrets (development)
dotnet user-secrets set "ConnectionStrings:Default" "Server=..."

Load Balancing and High Availability

Windows Network Load Balancing (NLB)

# Install NLB feature
Install-WindowsFeature -Name NLB -IncludeManagementTools

# Create NLB cluster
New-NlbCluster -InterfaceName "Ethernet" `
-OperationMode Multicast `
-ClusterPrimaryIP 192.168.1.100 `
-ClusterName "AppCluster"

# Add cluster port rule (HTTP)
Add-NlbClusterPortRule -HostName "AppCluster" `
-StartPort 80 `
-EndPort 80 `
-Protocol Tcp `
-Affinity Single

# Add cluster nodes
Add-NlbClusterNode -InterfaceName "Ethernet" `
-HostName "Server2" `
-NewNodeName "Server2" `
-NewNodeInterface "Ethernet"

Linux HAProxy

# Install HAProxy
sudo apt install haproxy

# Configure HAProxy
sudo nano /etc/haproxy/haproxy.cfg

HAProxy configuration:

global
log /dev/log local0
maxconn 4096

defaults
log global
mode http
option httplog
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms

frontend http_front
bind *:80
default_backend http_back

backend http_back
balance roundrobin
option httpchk GET /health
server server1 192.168.1.101:5000 check
server server2 192.168.1.102:5000 check
server server3 192.168.1.103:5000 check

Start HAProxy:

sudo systemctl enable haproxy
sudo systemctl start haproxy
sudo systemctl status haproxy

Monitoring and Logging

Windows Event Log

// Program.cs
builder.Logging.AddEventLog(settings =>
{
settings.SourceName = "YourApp";
settings.LogName = "Application";
});

Linux syslog

// Add Serilog
using Serilog;

builder.Host.UseSerilog((context, services, configuration) =>
{
configuration
.ReadFrom.Configuration(context.Configuration)
.WriteTo.Console()
.WriteTo.File("/var/log/your-app/app-.log", rollingInterval: RollingInterval.Day);
});

Health Checks

// Program.cs
builder.Services.AddHealthChecks()
.AddSqlServer(connectionString, name: "database")
.AddCheck<OutboxHealthCheck>("outbox");

app.MapHealthChecks("/health");
app.MapHealthChecks("/health/ready");

Monitor health:

# Windows
Invoke-WebRequest -Uri http://localhost/health

# Linux
curl http://localhost/health

Security Hardening

1. Run with Least Privilege

Windows:

  • Use ApplicationPoolIdentity (IIS)
  • Use dedicated service account (Windows Service)

Linux:

  • Use www-data or dedicated user
  • Never run as root

2. File System Permissions

Windows:

# Grant read/execute to app pool
$acl = Get-Acl "C:\inetpub\wwwroot\your-app"
$permission = "IIS APPPOOL\YourAppPool", "ReadAndExecute", "ContainerInherit,ObjectInherit", "None", "Allow"
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule $permission
$acl.SetAccessRule($accessRule)
Set-Acl "C:\inetpub\wwwroot\your-app" $acl

Linux:

# Set ownership
sudo chown -R www-data:www-data /opt/your-app

# Set permissions (read/execute for files, write for logs)
sudo chmod -R 755 /opt/your-app
sudo chmod -R 775 /opt/your-app/logs

3. Firewall Configuration

Windows:

# Allow HTTP
New-NetFirewallRule -DisplayName "Allow HTTP" `
-Direction Inbound `
-Protocol TCP `
-LocalPort 80 `
-Action Allow

# Allow HTTPS
New-NetFirewallRule -DisplayName "Allow HTTPS" `
-Direction Inbound `
-Protocol TCP `
-LocalPort 443 `
-Action Allow

Linux (ufw):

# Enable firewall
sudo ufw enable

# Allow HTTP/HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Allow SSH (important!)
sudo ufw allow 22/tcp

# Check status
sudo ufw status

Troubleshooting

Application Won't Start (Windows)

# Check IIS logs
Get-Content "C:\inetpub\wwwroot\your-app\logs\stdout*.log" -Tail 50

# Check Event Viewer
Get-EventLog -LogName Application -Newest 20

# Verify .NET runtime
dotnet --list-runtimes

# Test application manually
cd C:\inetpub\wwwroot\your-app
dotnet YourApp.dll

Application Won't Start (Linux)

# Check systemd status
sudo systemctl status your-app.service

# View logs
sudo journalctl -u your-app.service -f

# Check permissions
ls -la /opt/your-app

# Test application manually
cd /opt/your-app
./YourApp

Database Connection Issues

# Test SQL Server connection (Windows)
sqlcmd -S .\SQLEXPRESS -E -Q "SELECT @@VERSION"

# Linux
/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'YourPassword' -Q "SELECT @@VERSION"

Next Steps


See Also


Last Updated: 2026-01-01 Framework: Excalibur 1.0.0 Platforms: Windows Server 2019+, Linux (Ubuntu 20.04+, RHEL 8+)