PU - Single IIS Site with Multiple Nested Applications

Product: PowerShell Universal
Version: 3.4.4
Windows Server: 2019 

I have a website “Default Web Site”, i want to create 2 nested sites, one for each department.

https:/myserverHost.domain.com/puDepartment1/login
https:/myserverHost.domain.com/puDepartment2/login

i am unable to get the base url to change in each application folder. I used “Convert to Application” and the site will not load, i get 404. I did however try to change my base href like some of the old “Universal Dashboard” articles, without success. If i could get 1 site to load, then i can work on the other.

i found a base href in \Modules\Universal\index.html and \UniversalAutomation\index.html

my single site runs on port https/443 with other apps on like https:/myserverHost.domain.com/podeApiStuff/

i tried to host it on an internal port, however i couldn’t get the URL Rewrite to work…

Currently i just get a blank page, nothing loads, and there are no errors in my logs.

Everything works if i create a new website and put PU in the wwwroot of that site and make it the ONLY thing it hosts, just like the video’s and the documentation explains to do.

I really hoped that this would help me: Nested IIS Site not working

To simplify things and get it off my main website, i created a new site on a different port, still cannot get it to work.

[19:14:42 INF] User profile is available. Using 'C:\Users\svc-account-dev\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
2022-10-26 07:14:42 [INFO]  (Hangfire.BackgroundJobServer) Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage'
2022-10-26 07:14:43 [INFO]  (Hangfire.BackgroundJobServer) Using the following options for Hangfire Server:
    Worker count: 100
    Listening queues: 'default', 'myservername'
    Shutdown timeout: 00:00:15
    Schedule polling interval: 00:00:15

2022-10-26 07:14:43 [INFO]  (Hangfire.Server.BackgroundServerProcess) Server eisportalapp01d:14360:6b35059b successfully announced in 84.6882 ms
2022-10-26 07:14:43 [INFO]  (Hangfire.Server.BackgroundServerProcess) Server eisportalapp01d:14360:6b35059b is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler...
2022-10-26 07:14:43 [INFO]  (Hangfire.Server.BackgroundServerProcess) Server eisportalapp01d:14360:6b35059b all the dispatchers started
[19:14:43 INF] Application started. Press Ctrl+C to shut down.
[19:14:43 INF] Hosting environment: Production
[19:14:43 INF] Content root path: E:\inetpubPU\wwwroot\puDepartment1
[19:14:43 INF] Request starting HTTP/1.1 GET http://localhost:8877/puDepartment1/login - -
[19:14:43 INF] Request finished HTTP/1.1 GET http://localhost:8877/puDepartment1/login - - - 200 - text/html 194.7977ms
[19:14:43 INF] Request starting HTTP/1.1 GET http://localhost:8877/puDepartment1/admin/static/css/16.0fe8871e.chunk.css.map - -
[19:14:43 INF] Request starting HTTP/1.1 GET http://localhost:8877/puDepartment1/admin/static/css/main.62c7aafc.chunk.css.map - -
[19:14:43 INF] Executing PhysicalFileResult, sending file 'E:\inetpubPU\wwwroot\puDepartment1\UniversalAutomation\static/css/16.0fe8871e.chunk.css.map' with download name '' ...
[19:14:43 INF] Executing PhysicalFileResult, sending file 'E:\inetpubPU\wwwroot\puDepartment1\UniversalAutomation\static/css/main.62c7aafc.chunk.css.map' with download name '' ...
[19:14:43 INF] Request starting HTTP/1.1 GET http://localhost:8877/puDepartment1/admin/manifest.json - -
[19:14:43 INF] Request finished HTTP/1.1 GET http://localhost:8877/puDepartment1/admin/manifest.json - - - 200 - text/html 7.0348ms
[19:14:43 INF] Request finished HTTP/1.1 GET http://localhost:8877/puDepartment1/admin/static/css/main.62c7aafc.chunk.css.map - - - 200 - text/plain 35.1213ms
[19:14:43 INF] Request finished HTTP/1.1 GET http://localhost:8877/puDepartment1/admin/static/css/16.0fe8871e.chunk.css.map - - - 200 - text/plain 35.3203ms
2022-10-26 07:14:54 [INFO]  (Hangfire.Server.BackgroundServerProcess) Server eisportalapp01d:14360:6b35059b caught stopping signal...
2022-10-26 07:14:54 [INFO]  (Hangfire.Server.BackgroundServerProcess) Server eisportalapp01d:14360:6b35059b caught stopped signal...
2022-10-26 07:14:54 [INFO]  (Hangfire.Server.BackgroundServerProcess) Server eisportalapp01d:14360:6b35059b All dispatchers stopped
2022-10-26 07:14:54 [INFO]  (Hangfire.Server.BackgroundServerProcess) Server eisportalapp01d:14360:6b35059b successfully reported itself as stopped in 0.3011 ms
2022-10-26 07:14:54 [INFO]  (Hangfire.Server.BackgroundServerProcess) Server eisportalapp01d:14360:6b35059b has been stopped in total 203.0735 ms

1 Like

You’ll need to ensure that the data is being stored in 2 different places or you are using SQL persistence as the LiteDB database cannot be shared between instances. You will need to update appsettings.json in order to change those settings.

I’d consider using appsettings.json files from the command line and updating the web.config in each site to point to the appropriate appsettings.json file: Settings - PowerShell Universal

You will primarily need to change the data settings here: Settings - PowerShell Universal

In addition to the data stored differences, you will need to adjust the base URL setting.

  "Kestrel": {
    "Endpoints": {
      "HTTP": {
        "Url": "http://*:5000"
      }
    },
    "RedirectToHttps": "false",
    "UseHttpSys": "false",
    "BasePath": "/puDepartment1"
  },

This causes an error:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
    </handlers>
    <aspNetCore processPath="E:\inetpubPU\wwwroot\puDepartment1\Universal.Server.exe --appsettings E:\inetpubPU\wwwroot\puDepartment1\appsettings.json" arguments="E:\inetpubPU\wwwroot\puDepartment1\Universal.Server.dll" forwardWindowsAuthToken="false" stdoutLogEnabled="true" stdoutLogFile=".\logs\log" hostingModel="InProcess" />
        <security>
            <authentication>
                <windowsAuthentication enabled="false" />
            </authentication>
        </security>
  </system.webServer>
</configuration>
<!--ProjectGuid: 588ACF2E-9AE5-4DF1-BC42-BCE16A4C4EDE-->

Here is the error:

Application 'E:\inetpubPU\wwwroot\puDepartment1\' failed to start. Exception message:
Process path 'E:\inetpubPU\wwwroot\puDepartment1\Universal.Server.exe --appsettings E:\inetpubPU\wwwroot\puDepartment1\appsettings.json' doesn't have '.exe' extension.

You’ll need to configure it like this. I also am not sure what InProcess will do in this case so you may need to do OutOfProcess for hosting model.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
    </handlers>
    <aspNetCore processPath="E:\inetpubPU\wwwroot\puDepartment1\Universal.Server.exe" arguments="--appsettings E:\inetpubPU\wwwroot\puDepartment1\appsettings.json" forwardWindowsAuthToken="false" stdoutLogEnabled="true" stdoutLogFile=".\logs\log" hostingModel="InProcess" />
        <security>
            <authentication>
                <windowsAuthentication enabled="false" />
            </authentication>
        </security>
  </system.webServer>
</configuration>
<!--ProjectGuid: 588ACF2E-9AE5-4DF1-BC42-BCE16A4C4EDE-->

i changed it to OutOfProcess, that didn’t help;
–appsetings moved to the arguments fixed that issue, ty

Here is my logs now:

appsettings file exists: E:\inetpubPU\wwwroot\puDepartment1\appsettings.json
appsettings file exists: E:\inetpubPU\wwwroot\puDepartment1\appsettings.json
[08:26:36 INF] User profile is available. Using 'C:\Users\svc-dev\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
2022-10-27 08:26:36 [INFO]  (Hangfire.BackgroundJobServer) Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage'
2022-10-27 08:26:36 [INFO]  (Hangfire.BackgroundJobServer) Using the following options for Hangfire Server:
    Worker count: 100
    Listening queues: 'default', 'myServerName'
    Shutdown timeout: 00:00:15
    Schedule polling interval: 00:00:15
2022-10-27 08:26:36 [INFO]  (Hangfire.Server.BackgroundServerProcess) Server myServerName:18636:c64a877a successfully announced in 59.464 ms
2022-10-27 08:26:36 [INFO]  (Hangfire.Server.BackgroundServerProcess) Server myServerName:18636:c64a877a is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler...
2022-10-27 08:26:36 [INFO]  (Hangfire.Server.BackgroundServerProcess) Server myServerName:18636:c64a877a all the dispatchers started
[08:26:36 INF] Overriding endpoints defined via IConfiguration and/or UseKestrel() because PreferHostingUrls is set to true. Binding to address(es) 'http://127.0.0.1:34195' instead.
[08:26:36 INF] Now listening on: http://127.0.0.1:34195
[08:26:36 INF] Application started. Press Ctrl+C to shut down.
[08:26:36 INF] Hosting environment: Production
[08:26:36 INF] Content root path: E:\inetpubPU\wwwroot\puDepartment1
[08:26:36 INF] Request starting HTTP/1.1 GET http://myServerName.domain.com:8877/puDepartment1/login - -
[08:26:37 INF] Cookies was not authenticated. Failure message: Unprotect ticket failed
[08:26:37 INF] Request finished HTTP/1.1 GET http://myServerName.domain.com:8877/puDepartment1/login - - - 200 - text/html 202.4895ms
[08:26:37 INF] Request starting HTTP/1.1 GET http://myServerName.domain.com:8877/puDepartment1/admin/static/css/16.0fe8871e.chunk.css.map - -
[08:26:37 INF] Cookies was not authenticated. Failure message: Unprotect ticket failed
[08:26:37 INF] Request starting HTTP/1.1 GET http://myServerName.domain.com:8877/puDepartment1/admin/static/css/main.62c7aafc.chunk.css.map - -
[08:26:37 INF] Cookies was not authenticated. Failure message: Unprotect ticket failed
[08:26:37 INF] Executing PhysicalFileResult, sending file 'E:\inetpubPU\wwwroot\puDepartment1\UniversalAutomation\static/css/16.0fe8871e.chunk.css.map' with download name '' ...
[08:26:37 INF] Executing PhysicalFileResult, sending file 'E:\inetpubPU\wwwroot\puDepartment1\UniversalAutomation\static/css/main.62c7aafc.chunk.css.map' with download name '' ...
[08:26:37 INF] Request finished HTTP/1.1 GET http://myServerName.domain.com:8877/puDepartment1/admin/static/css/16.0fe8871e.chunk.css.map - - - 200 - text/plain 21.2259ms
[08:26:37 INF] Request finished HTTP/1.1 GET http://myServerName.domain.com:8877/puDepartment1/admin/static/css/main.62c7aafc.chunk.css.map - - - 200 - text/plain 8.0021ms
2022-10-27 08:26:46 [INFO]  (Hangfire.Server.BackgroundServerProcess) Server myServerName:18636:c64a877a caught stopping signal...
2022-10-27 08:26:46 [INFO]  (Hangfire.Server.BackgroundServerProcess) Server myServerName:18636:c64a877a caught stopped signal...
2022-10-27 08:26:46 [INFO]  (Hangfire.Server.BackgroundServerProcess) Server myServerName:18636:c64a877a All dispatchers stopped
2022-10-27 08:26:46 [INFO]  (Hangfire.Server.BackgroundServerProcess) Server myServerName:18636:c64a877a successfully reported itself as stopped in 0.2998 ms
2022-10-27 08:26:46 [INFO]  (Hangfire.Server.BackgroundServerProcess) Server myServerName:18636:c64a877a has been stopped in total 11.6479 ms

it still only loads a blank screen :frowning:

Let me run through this locally to ensure I’m not missing something.

You’re the BEST! ty

I got it to work and here is my current configuration.

Single App Pool with a single website and 2 applications.

image

This is the folder structure.

image

psu1 and psu2 contain the application files.

image

data1 and data2 are used for storing the PSU configuration data and database.

image

The appsettings.json files configure the data storage and base URLs.

This is appsettings.psu1.json.

{
  "Kestrel": {
    "Endpoints": {
      "HTTP": {
        "Url": "http://*:5000"
      }
    },
    "RedirectToHttps": "false",
    "UseHttpSys": "false",
    "BasePath": "/psu1"
  },
  "ApplicationInsights": {
    "InstrumentationKey": ""
  },
  "Logging": {
    "Path": "C:\\src\\psu\\data1\\log.txt",
    "RetainedFileCountLimit": 31,
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Grpc": "Information"
    }
  },
  "AllowedHosts": "*",
  "CorsHosts": "",
  "Plugins": [
    "UniversalAutomation.LiteDBv5"
  ],
  "Data": {
    "RepositoryPath": "C:\\src\\psu\\data1\\Repository",
    "ConnectionString": "filename=C:\\src\\psu\\data1\\database.db;upgrade=true",
    "RunMigrations": true,
    "GitRemote": "",
    "GitUserName": "",
    "GitPassword": "",
    "GitBranch": "",
    "GitSyncBehavior": "TwoWay",
    "GitInitializeBehavior": "",
    "GitSyncInterval": "1",
    "ConfigurationScript": "",
    "Mode": "Manual"
  },
  "Api": {
    "Url": "",
    "GrpcPort": 0
  },
  "Authentication": {
    "Windows": {
      "Enabled": "false"
    },
    "WSFed": {
      "Enabled": "false",
      "MetadataAddress": "",
      "Wtrealm": "",
      "CallbackPath": "/auth/signin-wsfed",
      "Wreply": "",
      "UseTokenLifetime": true,
      "CorrelationCookieSameSite": ""
    },
    "OIDC": {
      "Enabled": "false",
      "CallbackPath": "/auth/signin-oidc",
      "ClientID": "",
      "ClientSecret": "",
      "Resource": "",
      "Authority": "",
      "ResponseType": "",
      "SaveTokens": "false",
      "CorrelationCookieSameSite": "",
      "UseTokenLifetime": true,
      "Scope": "openid profile groups",
      "GetUserInfo": false
    },
    "ClientCertificate": {
      "Enabled": "false"
    },
    "SessionTimeout": "25"
  },
  "Jwt": {
    "SigningKey": "PleaseUseYourOwnSigningKeyHere",
    "Issuer": "IronmanSoftware",
    "Audience": "PowerShellUniversal"
  },
  "UniversalAutomation": {
    "JobHandshakeTimeout": 5,
    "JobDebugging": false,
    "ContinueJobOnServerStop": false
  },
  "UniversalDashboard": {
    "AssetsFolder": "%ProgramData%\\PowerShellUniversal\\Dashboard",
    "DashboardStartupTimeout": 10
  },
  "Secrets": {
    "SecretStore": {
      "Password": "PSUSecretStore"
    }
  },
  "ShowDevTools": false,
  "HideAdminConsole": false,
  "Profiling": false
}

This is appsettings.psu2.json.

{
  "Kestrel": {
    "Endpoints": {
      "HTTP": {
        "Url": "http://*:5000"
      }
    },
    "RedirectToHttps": "false",
    "UseHttpSys": "false",
    "BasePath": "/psu2"
  },
  "ApplicationInsights": {
    "InstrumentationKey": ""
  },
  "Logging": {
    "Path": "C:\\src\\psu\\data2\\log.txt",
    "RetainedFileCountLimit": 31,
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Grpc": "Information"
    }
  },
  "AllowedHosts": "*",
  "CorsHosts": "",
  "Plugins": [
    "UniversalAutomation.LiteDBv5"
  ],
  "Data": {
    "RepositoryPath": "C:\\src\\psu\\data2\\Repository",
    "ConnectionString": "filename=C:\\src\\psu\\data2\\database.db;upgrade=true",
    "RunMigrations": true,
    "GitRemote": "",
    "GitUserName": "",
    "GitPassword": "",
    "GitBranch": "",
    "GitSyncBehavior": "TwoWay",
    "GitInitializeBehavior": "",
    "GitSyncInterval": "1",
    "ConfigurationScript": "",
    "Mode": "Manual"
  },
  "Api": {
    "Url": "",
    "GrpcPort": 0
  },
  "Authentication": {
    "Windows": {
      "Enabled": "false"
    },
    "WSFed": {
      "Enabled": "false",
      "MetadataAddress": "",
      "Wtrealm": "",
      "CallbackPath": "/auth/signin-wsfed",
      "Wreply": "",
      "UseTokenLifetime": true,
      "CorrelationCookieSameSite": ""
    },
    "OIDC": {
      "Enabled": "false",
      "CallbackPath": "/auth/signin-oidc",
      "ClientID": "",
      "ClientSecret": "",
      "Resource": "",
      "Authority": "",
      "ResponseType": "",
      "SaveTokens": "false",
      "CorrelationCookieSameSite": "",
      "UseTokenLifetime": true,
      "Scope": "openid profile groups",
      "GetUserInfo": false
    },
    "ClientCertificate": {
      "Enabled": "false"
    },
    "SessionTimeout": "25"
  },
  "Jwt": {
    "SigningKey": "PleaseUseYourOwnSigningKeyHere",
    "Issuer": "IronmanSoftware",
    "Audience": "PowerShellUniversal"
  },
  "UniversalAutomation": {
    "JobHandshakeTimeout": 5,
    "JobDebugging": false,
    "ContinueJobOnServerStop": false
  },
  "UniversalDashboard": {
    "AssetsFolder": "%ProgramData%\\PowerShellUniversal\\Dashboard",
    "DashboardStartupTimeout": 10
  },
  "Secrets": {
    "SecretStore": {
      "Password": "PSUSecretStore"
    }
  },
  "ShowDevTools": false,
  "HideAdminConsole": false,
  "Profiling": false
}

The web.config files are configured to point to the appsettings.json files for each application. OutOfProcess is already required by ASP.NET Core so I had to change to that.

This is the web.config from C:\src\psu\psu1.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
    </handlers>
    <aspNetCore processPath=".\Universal.Server.exe" arguments="--appsettings C:\src\psu\appsettings.psu1.json" forwardWindowsAuthToken="false" stdoutLogEnabled="true" stdoutLogFile=".\logs\log" hostingModel="OutOfProcess" />
  </system.webServer>
</configuration>
<!--ProjectGuid: 588ACF2E-9AE5-4DF1-BC42-BCE16A4C4EDE-->

This is the web.config from C:\src\psu\psu2

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
    </handlers>
    <aspNetCore processPath=".\Universal.Server.exe" arguments="--appsettings C:\src\psu\appsettings.psu2.json" forwardWindowsAuthToken="false" stdoutLogEnabled="true" stdoutLogFile=".\logs\log" hostingModel="OutOfProcess" />
  </system.webServer>
</configuration>
<!--ProjectGuid: 588ACF2E-9AE5-4DF1-BC42-BCE16A4C4EDE-->

And now I can navigate to http://localhost/psu1/admin

image

And I can also navigate to http://localhost/psu2/admin

image

I edited the docs to include this info: https://docs.powershelluniversal.com/config/hosting/hosting-iis#nested-iis-applications

OK, now that i got your example to work i am going to try and create my own site’s and try not to mess things up. Thank you for looking at this for me!

1 Like

So, i created a script that matches your deployment and you can change the folder and the name at the top of the script as long as you have the zip in the correct folder:

$here = "E:\src"
$appName = "dep1"
$zipFilename = "Universal.win7-x64.3.4.4.zip";

$here = $PSScriptRoot

$iisRoot = "$here\psu"
$zip = "$here\$zipFilename"

$appPath = Join-Path -Path $iisRoot -ChildPath $appName # C:\inetpub\wwwroot\myappName
$dataPath = $appPath + "_data"
mkdir $dataPath -ErrorAction SilentlyContinue
mkdir $appPath -ErrorAction SilentlyContinue
cd $here

Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory("$zip", $appPath)
Get-ChildItem $appPath -Recurse | Unblock-File

$appSettings = "$iisRoot\appsettings.$appName.json"
Copy-Item -Path "$appPath\appsettings.json" -Destination $appSettings 

# Save app settings to new location
$jsonData = Get-Content $appSettings | ConvertFrom-Json
$jsonData.Kestrel.BasePath = "/$appName"
$jsonData.Api.Url = "/$appName"
$jsonData.Data.RepositoryPath = Join-Path -Path $dataPath -ChildPath "UniversalAutomation\Repository"
$jsonData.data.ConnectionString = "filename=$dataPath\UniversalAutomation\database.db;upgrade=true"
$jsonData.UniversalDashboard.AssetsFolder = Join-Path $dataPath -ChildPath "PowerShellUniversal\Dashboard"
$jsonData.Logging.Path = Join-Path $dataPath -ChildPath "PowerShellUniversal\log.txt"
$jsonData | ConvertTo-Json -Depth 30 | Set-Content $appSettings

$webconfigFile = Join-Path -Path $appPath -ChildPath 'web.config'
$webconfig = (Get-Content $webconfigFile) -as [xml]
$webconfig.configuration.'system.webServer'.aspNetCore.arguments = "--appsettings $appSettings"
$webconfig.configuration.'system.webServer'.aspNetCore.hostingModel = "OutOfProcess"
$webconfig.Save("$webconfigFile")
1 Like

errr @adam, i have a problem, everything so far seems to work but notifications

When you click ‘View All’ notifications it goes here, why isn’t it adding the sub site in for notifications?
https://servername.domain.com/admin/notifications

@adam, maybe i found a bug? It is not adding the site into the href for the admin notifications

image

I can reproduce this and this will be resolved in 3.5.

@adam, you’re the best x2.

Sorry for the noob question, but what are the benefit/use case with nested vs seperate sites?

it is for https for 1 cert to be used instead of having to create nth number of ssl certs, we run on https.

1 Like

Interesting, thanks.

The downside being unable to restart/recycle a single site i guess, with a shared app pool?

You can split the app pools, that is no problem. It is a good example for lumping everything together tho :wink:

@adam , i think i found another problem, it doesn’t visit the nested site when you click the header:

Created a bug here for you: Links Not working when using nested IIS application configuration · Issue #1661 · ironmansoftware/issues · GitHub