Deploy PowerShell Universal as a Docker Image (via Azure App Service)

Product: PowerShell Universal
Version: 3.5.5

@adam and team,

Thanks for having a look at my issue. I am a bit green on the Docker front.

I am attempting to deploy my working localhost configuration of PSU to a Docker Container.

Effectively, I created a local folder that has the following folders/files (I copied them to .docker folder as I realized the dockerfile statements were only copying from the location of my dockerfile):

image

dockerfile:

appsettings.json

{
    "Kestrel": {
        "Endpoints": {
            "HTTP": {
                "Url": "http://*:443"
            }
        },
        "RedirectToHttps": "false",
        "UseHttpSys": "false",
        "BasePath": ""
    },
    "ApplicationInsights": {
        "InstrumentationKey": ""
    },
    "Logging": {
        "Path": "%PROGRAMDATA%/PowerShellUniversal/log.txt",
        "RetainedFileCountLimit": 31,
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information",
            "Grpc": "Information"
        }
    },
    "AllowedHosts": "*",
    "CorsHosts": "",
    "Plugins": [
        "UniversalAutomation.LiteDBv5"
    ],
    "Data": {
        "RepositoryPath": "%HOME%/data/Repository",
        "ConnectionString": "filename=%HOME%/data/Repository/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": "true",
            "CallbackPath": "/auth/signin-oidc",
            "ClientID": "<REDACTED>",
            "ClientSecret": "<REDACTED>",
            "Resource": "",
            "Authority": "https://login.microsoftonline.com/<REDACTED>",
            "ResponseType": "code",
            "SaveTokens": "false",
            "CorrelationCookieSameSite": "",
            "UseTokenLifetime": true,
            "Scope": "openid profile groups",
            "GetUserInfo": true
        },
        "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
}

Configuration

When I mount the container I see my files including appsettings.json

Each DOCKER cmd:

What am I missing? Your input would be greatly appreciated.

Thank you,

Kevin

I think I missed the Docker - PowerShell Universal doc. trying the mount point now. However I can’t tell if that is only for Windows because it lives in that section.

1 Like

You’ll need the mount in order to make the data persistent. If you want to make it none persistent, then you can just copy the data in and skip the volume command.

If you are hosting this in Azure, you can set the mount points directly in the portal: https://docs.powershelluniversal.com/config/hosting/azure#azure-storage-and-litedb

You’d then set the appsettings env var directly in the Azure portal configuration page.

1 Like

Thanks Adam,

  • We are attempting to start from scratch.
  • We moved the docker file to the root of UniversalAutomation directory. This our functioning localhost install.
  • We are attempting to host in Azure
  • We do want to make the data persistent

Our dockerfile looks like this:

FROM ironmansoftware/universal:latest
LABEL description="Universal - The ultimate platform for building web-based IT Tools"

EXPOSE 5000
VOLUME ["/home/data"]

COPY . /home/data/

COPY appsettings.json /home/data/Universal/appsettings.json
COPY appsettings.json /home/Universal/appsettings.json

COPY appsettings.linux.json /home/Universal/appsettings.linux.json
COPY appsettings.linux.json /home/data/Universal/appsettings.linux.json

ENV Data__RepositoryPath /home/data/Repository
ENV Data__ConnectionString /home/data/database.db
ENV UniversalDashboard__AssetsFolder /home/data/UniversalDashboard
ENV Logging__Path /home/data/logs/log.txt
ENTRYPOINT ["./Universal/Universal.Server"]

Q: Can we use port 443 to host our PSU? Would that be internal port 5000 and external 443 so no port needs to be specified by the user? Where should we specify that configuration?

Q: Do we need copy the persistent data via the dockerfile. And if so, which files?

Q: For a Linux docker container in an Azure WebApp where do we need to copy the appsettings.json (or is it the appsettings.linux.json)?

Note: We noticed, in your base image, each is stored in Universal folder:

Q: What variables (environment or otherwise) need to live in the Configuration of the WebApp, the json file (appsettings.json or appsettings.linux.json), the dockerfile, or the file share?

Our appsettings.json
Q: does this look correct?

{
    "Kestrel": {
        "Endpoints": {
            "HTTP": {
                "Url": "http://*:443"
            }
        },
        "RedirectToHttps": "false",
        "UseHttpSys": "false",
        "BasePath": ""
    },
    "ApplicationInsights": {
        "InstrumentationKey": ""
    },
    "Logging": {
        "Path": "%PROGRAMDATA%/PowerShellUniversal/log.txt",
        "RetainedFileCountLimit": 31,
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information",
            "Grpc": "Information"
        }
    },
    "AllowedHosts": "*",
    "CorsHosts": "",
    "Plugins": [
        "UniversalAutomation.LiteDBv5"
    ],
    "Data": {
        "RepositoryPath": "%HOME%/data/Repository",
        "ConnectionString": "filename=%HOME%/data/Repository/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": "true",
            "CallbackPath": "/auth/signin-oidc",
            "ClientID": "<redacted>",
            "ClientSecret": "<redacted>",
            "Resource": "",
            "Authority": "https://login.microsoftonline.com/<redacted>",
            "ResponseType": "code",
            "SaveTokens": "false",
            "CorrelationCookieSameSite": "",
            "UseTokenLifetime": true,
            "Scope": "openid profile groups",
            "GetUserInfo": true
        },
        "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
}

Our appsettings.linux.json

Q: is this merely a template or is PSU looking for this particular file named appsettings.linux.json (or should it be renamed and replace the appsettings.json in a linux environment)?

Note: We noticed the appsettings.linux.json references the following path

"RepositoryPath": "%HOME%/.PowerShellUniversal/Repository",
"ConnectionString": "%HOME%/.PowerShellUniversal/database.db",

Q: Is the path correct? Do we use that path or another?

{
    "Kestrel": {
        "Endpoints": {
            "HTTP": {
                "Url": "http://*:5000"
            }
        },
        "RedirectToHttps": "false"
    },
    "Logging": {
        "Path": "%HOME%/.PowerShellUniversal/log.txt",
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    },
    "Plugins": [
        "UniversalAutomation.LiteDBv5"
    ],
    "AllowedHosts": "*",
    "Data": {
        "RepositoryPath": "%HOME%/.PowerShellUniversal/Repository",
        "ConnectionString": "%HOME%/.PowerShellUniversal/database.db",
        "RunMigrations": true,
        "DatabaseType": "LiteDB",
        "GitRemote": "",
        "GitUserName": "",
        "GitPassword": "",
        "GitBranch": "",
        "GitSyncBehavior": "TwoWay",
        "GitInitializeBehavior": "",
        "GitSyncInterval": "1",
        "ConfigurationScript": "",
        "ExternalGitClient": false,
        "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": "true",
            "CallbackPath": "/auth/signin-oidc",
            "ClientID": "<redacted>",
            "ClientSecret": "<redacted>",
            "Resource": "",
            "Authority": "https://login.microsoftonline.com/<redacted>",
            "ResponseType": "code",
            "SaveTokens": "false",
            "CorrelationCookieSameSite": "",
            "UseTokenLifetime": true,
            "Scope": "openid profile groups",
            "GetUserInfo": true
        },
        "ClientCertificate": {
            "Enabled": "false"
        },
        "SessionTimeout": "25"
    },
    "Jwt": {
        "SigningKey": "PleaseUseYourOwnSigningKeyHere",
        "Issuer": "IronmanSoftware",
        "Audience": "PowerShellUniversal"
    },
    "UniversalAutomation": {
        "JobHandshakeTimeout": 5,
        "JobDebugging": false,
        "ContinueJobOnServerStop": false
    },
    "UniversalDashboard": {
        "AssetsFolder": "%HOME%/.PowerShellUniversal/Dashboard",
        "DashboardStartupTimeout": 10
    },
    "Secrets": {
        "SecretStore": {
            "Password": "PSUSecretStore"
        }
    },
    "ShowDevTools": false,
    "HideAdminConsole": false,
    "Profiling": false
}

In the documentation should the case be lowercase in the bottom image?

Thank you for your support @adam

@adam I think I have narrowed it down to the files never appearing in the mapping but hoping you can inspect the images below to verify. Really hoping to get this portion behind us so we can demo internally. Note, we can copy the files into the share with Azure Storage Explorer and everything works perfectly. However, that won’t scale of course.

It seems, in the local container, the files specified in the dockerfile, do copy:

However, when I push, the file share is never populated. When I restart the app service, PSU creates a new database.db and database-log.db files. We instead are looking for the original .db files and the Repository folder recursively.

dockerfile

Application Settings

Path Mappings

File Share Settings

Thanks in advance for your help.

@adam if we don’t copy the data in via dockerfile, how does the repository data and the DB get added to the container?

Does this process use Azure file shares where we map via port 445 to my local PC?

Yep. This process uses the Azure file shares. You would map them locally, copy them in and then attach them in the Azure web app. You don’t even need to copy in the appsettings.json since you can set those values in the Configuration page because they will be passed in as environment variables.

It appears AT&T doesn’t allow Port 445 outbound. Should we be looking at SQL/GIT?

Anyone else facing this with AT&T

Twitter or Chat Support - wont handle it
(800)288-2020 - is who to contact. Then you need back office support team

They are working on my request to open Port 445 outbound. Ironically, the tool they use to do so is down. Nothing can be changed on your AT&T device, no technician sent to your house will be able to help. Just the back office team. I was escalated from the initial call-center service representative, then after pleading, I made it to a supervisor. The supervisor got the back-office team involved. I will update once its actually complete.

I do have a question @adam - how do I handle source control. If I am reading correctly:

Per: Azure - PowerShell Universal
Do not use an Azure Storage Account if you chose to use git integration. PowerShell Universal performance will be severely degraded if you use this configuration due to the performance of Azure File Shares.

EDIT: AT&T wont open port 445. I am going to switch to 3.6.2 instead

When you configure SQL and git, it will use the local storage of the Azure web app and not the mounted File Share. The problem is that, for whatever reason, we’ve seen git commits time out or completely hang when trying to use an Azure File Share.

With the SQL configuration, the git settings can be stored in SQL so when you connect a new node to the database, it will then clone the repository locally and configure itself.

Do note, that in 3.6 (out this Tuesday), we will be introducing SQL-based git storage. We use the git bundle feature to store the git repository directly in SQL so there isn’t even a need to setup a remote git instance. The nodes will clone directly from SQL.

1 Like

Super excited for 3.6!

@adam are these instructions that we are to follow for this new feature?