We have successfully done this using Docker. For example, we use the following docker image as base:
mcr.microsoft.com/dotnet/framework/aspnet:4.8-windowsservercore-ltsc2019
Note that Microsoft no longer hosts these images on docker hub.
Here is an example of rather complicated one. As you can see, we build the image from a pre-compiled ASP.NET application, install some IIS modules and even pull an installer for a compression module's extension.
FROM mcr.microsoft.com/dotnet/framework/aspnet:4.8-windowsservercore-ltsc2019
COPY OurSite c:/OurSite
# Enable IIS's Static Content, Static Content Compression and App Init modules
# Download & Install IIS compression extension (providing Brotli)
# Setting "staticCompressionIgnoreHitFrequency" attribute on "HttpCompression" tag in applicationHost.config (that's the only place we can set that attr)
# Remove Default Web Site
# Register a new site
# Set following properties on the Application Pool:
# - "Start Mode" to "AlwaysRunning" to ensure w3wp process always starts
# - "Idle Timeout" to 0 ensuring w3wp process doesn't get terminated due to inactivity.
# Recycle Application Pool
RUN Enable-WindowsOptionalFeature -Online -FeatureName IIS-ApplicationInit ; \
Enable-WindowsOptionalFeature -Online -FeatureName IIS-StaticContent ; \
Enable-WindowsOptionalFeature -Online -FeatureName IIS-HttpCompressionStatic ; \
Invoke-WebRequest -Uri https://download.microsoft.com/download/6/1/C/61CC0718-ED0E-4351-BC54-46495EBF5CC3/iiscompression_amd64.msi \
-OutFile c:\iiscompression_amd64.msi ; \
Start-Process msiexec.exe -ArgumentList '-i', 'c:\iiscompression_amd64.msi', '/quiet', '/passive' -NoNewWindow -Wait ; \
Remove-Item c:\iiscompression_amd64.msi -Force ; \
$ConfigSection = Get-IISConfigSection -SectionPath "system.webServer/httpCompression" ; \
Set-IISConfigAttributeValue -ConfigElement $ConfigSection -AttributeName "staticCompressionIgnoreHitFrequency" -AttributeValue True ; \
Remove-WebSite -Name 'Default Web Site' ; \
New-Website -Name 'OurSite' -Port 80 -PhysicalPath 'c:\OurSite' -ApplicationPool '.NET v4.5' ; \
Import-Module WebAdministration ; \
Set-ItemProperty -Path 'IIS:\AppPools\.NET v4.5' -Name 'startMode' -Value 'AlwaysRunning' ; \
Set-ItemProperty -Path 'IIS:\Sites\OurSite' -Name 'applicationDefaults.preloadEnabled' -Value 'true' ; \
Set-ItemProperty -Path 'IIS:\AppPools\.NET v4.5' -Name 'processModel.idleTimeout' -Value '00:00:00' ; \
Restart-WebAppPool '.NET v4.5'
VOLUME c:\\data
EXPOSE 80
We then host these images in a secured Container Registry, Create a Service Fabric project (in Visual Studio) which gives us the template for the required manifests using which we'll deploy as many containers as we need. At the deployment time, Service Fabric reads your manifest, pulls and caches the docker image and creates the containers accordingly. It goes without saying that your manifests will have to contain information about the registry and associated credentials.
Side note: never use the "latest" tag of your images in the manifests, because, as mentioned, Service Fabric caches images. Also, remember to configure your cluster to clean up old images. Otherwise, depending on your application, you might run outta space quickly.
Update: Storing docker images in Aure ACR, one can use Managed Service Identity and avoid storing the registry's credentials in the manifests. The Identity assigned to the cluster's underlying VM Scale Sets can be given access to ACR to download docker images during deployment. This ensures the credentials are not seen by anyone having access to the SF's explorer dashboard (and one doesn't have to worry about encrypting the secrets in manifest).