Skip to content

Application Settings

This document describes all configurable settings available in appsettings.extra.json.

Configuration File Location: cv4pve-admin loads the appsettings.extra.json file from its config/ directory. In Docker the directory is mounted as /app/config/. With the default .env the host path is data/cv4pve-admin/config/appsettings.extra.json. The file is optional: use it to override default settings without modifying the main appsettings.json file.

  • If the file does not exist, it is created at startup with an empty configuration ({}) and the application starts with default settings.
  • If the file contains invalid JSON, it is ignored: the application starts with default settings and writes a warning in the log.
  • Restart the application after editing the file, so that every setting is applied.

Merge behaviour:

  • Scalar and object properties: merged. Only the keys you specify are overridden, the rest keep their default values.
  • Precedence: appsettings.extra.json is loaded last, so its values win over appsettings.json and also over environment variables. For example, a ConnectionStrings:DefaultConnection written in this file replaces the one that Docker Compose passes with ConnectionStrings__DefaultConnection.
  • JSON arrays: merged by position, not replaced. The first element you write overrides the first default element, the second overrides the second, and default elements at positions you do not write are kept. For an array such as Serilog.WriteTo, write the entries in the same order as the defaults and change only the values you need.

Any setting can also be passed as an environment variable, replacing : with a double underscore: for example CookieSettings__ExpireDays=7.

{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=cv4pve_admin;Username=postgres;Password=yourpassword"
}
}
Setting Description
DefaultConnection PostgreSQL connection string used by Entity Framework, Hangfire, and the Serilog PostgreSQL sink

There is no default value: the connection string must be provided. In a Docker installation the compose file already sets it from the POSTGRES_* variables of .env, so you normally do not need to write it here.

{
"CookieSettings": {
"ExpireDays": 30
}
}
Setting Default Description
ExpireDays 30 Number of days before the authentication cookie expires. The expiration is sliding: it is renewed while the user keeps using the application. The same number of days applies to a browser remembered at the two-factor login (Remember Machine), without renewal.

Password rules, lockout rules and the limit of sign-in attempts, under Security: the sections PasswordOptions and LockoutOptions, and the key MaxAttemptsPerMinute. Each of the two sections is applied only if it is present in the configuration. Inside a section, the keys you omit keep the default shown in the table.

{
"Security": {
"PasswordOptions": {
"RequiredLength": 6,
"RequireNonAlphanumeric": true,
"RequireDigit": true,
"RequireLowercase": true,
"RequireUppercase": true
}
}
}
Setting Default Description
RequiredLength 6 Minimum password length
RequireNonAlphanumeric true Require special characters (!@#$%^&* etc.)
RequireDigit true Require at least one digit (0-9)
RequireLowercase true Require at least one lowercase letter
RequireUppercase true Require at least one uppercase letter

The rules are checked when a password is set or changed. Existing passwords are not affected.

{
"Security": {
"LockoutOptions": {
"MaxFailedAccessAttempts": 5,
"AllowedForNewUsers": true,
"DefaultLockoutTimeSpan": "00:05:00"
}
}
}
Setting Default Description
MaxFailedAccessAttempts 5 Number of failed login attempts before lockout
AllowedForNewUsers true Enable lockout for new users
DefaultLockoutTimeSpan 00:05:00 Duration of account lockout (format: HH:mm:ss). A value of 00:00:00 is replaced with 15 minutes.

Failed logins count towards the lockout only for accounts that have lockout enabled. A locked account can be unlocked before the time expires with the user unlock command of the CLI.

{
"Security": {
"MaxAttemptsPerMinute": 10
}
}
Setting Default Description
MaxAttemptsPerMinute 10 Sign-in attempts accepted from one address in a minute, on the login page and on the two-factor page, counted separately for each page. Past the limit the page answers “Too many attempts” without checking the password, also a right one. The same number limits the failed authentications on the MCP endpoint of AI Server from one address: past the limit it answers 429 and writes no audit row. 0 turns the limit off.

The lockout protects one account, this limit protects the server: the domain login (LDAP, Active Directory) has no local lockout, and without a limit the attempts could also lock the accounts of the directory. Behind a reverse proxy the address is the one the proxy forwards: set TrustedProxies so that a client cannot write its own.

{
"PublicUrl": "https://admin.example.com/",
"TrustedProxies": [ "172.16.0.0/12", "192.168.0.10" ]
}

PublicUrl

The address users open in the browser, with the scheme and, if any, the path under which the application is published. The links in the emails sent by the application (email confirmation, password reset) are built from it. EE The callback address shown in External Authentication uses it too.

Without it the links take the address from the request that sends the email, and the log shows PublicUrl is not set at startup. Set it in every installation reachable by more than one address, or from outside your network: the host of a request can be chosen by the client. A value that is not an absolute http or https address is ignored, with a warning in the log.

  • Default: (none)
  • Environment variable: PublicUrl=https://admin.example.com/

TrustedProxies

Addresses (192.168.0.10) or networks (172.16.0.0/12) of the reverse proxies whose X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host headers are accepted. Loopback is always accepted. The headers of any other address are ignored: the application sees the address and the scheme of the request as they arrive.

Without the setting, or with "*", the headers are accepted from any address. This is the behaviour of the previous versions, and it works with any proxy. List your proxy when the application port can be reached also without it. For a proxy in the same Docker Compose project, or on the Docker host, the network of Docker is usually inside 172.16.0.0/12. An entry that is neither an address nor a network is ignored, with a warning in the log. At startup the log lists the accepted entries.

  • Default: (none, headers accepted from any address)
  • Environment variables: TrustedProxies__0=172.16.0.0/12, TrustedProxies__1=192.168.0.10

When the proxy is not in the list, the application sees plain HTTP behind an HTTPS proxy. The sign-in with an external provider then fails, because the provider receives an http callback address, and the audit shows the address of the proxy for every user.

{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Warning",
"System": "Warning",
"System.Net.Http.HttpClient": "Warning",
"LibGit2Sharp": "Warning",
"ZiggyCreatures.Caching.Fusion": "Warning",
"Polly": "Warning",
"Corsinvest.ProxmoxVE.Api": "Warning"
}
},
"Properties": {
"Application": "cv4pve-admin"
},
"WriteTo": [...]
}
}
  • Verbose - Most detailed logging
  • Debug - Debugging information
  • Information - General information (default)
  • Warning - Warnings and potential issues
  • Error - Errors that need attention
  • Fatal - Critical failures

To change only the log level, override the single key and leave the rest alone:

{
"Serilog": {
"MinimumLevel": {
"Default": "Debug"
}
}
}

The default WriteTo array contains, in this order: File (position 0), Console (position 1) and, in the Enterprise Edition, PostgreSQL (position 2).

{
"Name": "File",
"Args": {
"path": "./data/logs/log-.txt",
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} {ClientIp} {UserName}{NewLine}{Exception}",
"rollingInterval": "Day",
"retainedFileCountLimit": 30
}
}
Setting Description
path Log file path (supports rolling with date placeholder)
outputTemplate Log message format
rollingInterval Rolling interval (Hour, Day, Month, Year)
retainedFileCountLimit Number of log files to retain
{
"Name": "Console",
"Args": {
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} {ClientIp} {UserName} <s:{SourceContext}>{NewLine}{Exception}"
}
}
{
"Name": "PostgreSQL",
"Args": {
"connectionString": "DefaultConnection",
"tableName": "Logs",
"schemaName": "logs_serilog",
"needAutoCreateTable": true,
"needAutoCreateSchema": true,
"loggerColumnOptions": {
"Id": { "Name": "IdAutoIncrement" },
"Timestamp": { "Name": "Timestamp" },
"Level": { "Name": "Level" },
"Message": { "Name": "RenderedMessage" },
"MessageTemplate": { "Name": "Message" },
"Exception": { "Name": "Exception" },
"LogEvent": { "Name": "LogEvent" }
}
}
}
Setting Description
connectionString Connection string name from ConnectionStrings section
tableName Database table name for logs
schemaName Database schema name
needAutoCreateTable Auto-create table if not exists
needAutoCreateSchema Auto-create schema if not exists
loggerColumnOptions Column mapping configuration

The log viewer in Monitoring → Logs reads the table and schema name from the Args of the sink named PostgreSQL in Serilog.WriteTo, whatever its position: tableName and schemaName. When they are missing, it falls back to Logs and logs_serilog.

Integration with the companion containers of the Docker Compose installation. The compose files set these values with environment variables (Container__Watchtower__Url, …), so you normally do not need to write them in appsettings.extra.json.

{
"Container": {
"Watchtower": {
"Url": "http://watchtower:8080",
"Token": "your-watchtower-token"
},
"Apprise": {
"Url": "http://apprise:8000"
}
}
}

Container:Watchtower:Url

Address of the Watchtower HTTP API. When it is set, cv4pve-admin looks up new versions on Docker Hub. The Update button of the help menu can then trigger the update of the container (not available in pre-release builds). When it is empty, cv4pve-admin looks up new versions on GitHub and you update manually.

  • Default: (none)

Container:Watchtower:Token

Token sent to the Watchtower HTTP API. Must match WATCHTOWER_HTTP_API_TOKEN of the Watchtower container.

  • Default: (none)

Container:Apprise:Url EE

Address of the Apprise API server. It pre-fills the Endpoint of a new Apprise configuration in the Notification Hub.

  • Default: (none)
{
"DetailedErrors": false,
"AllowedHosts": "*"
}

DetailedErrors

Send the details of an error of the interface to the browser. Turn it on only to troubleshoot, then off again: the details can contain internal information.

  • Default: false

AllowedHosts

Allowed host headers (* for all)

  • Default: *

Urls

Addresses the web server listens on. The Docker image sets it with the ASPNETCORE_URLS environment variable. EE The Workflow module requires this setting to be present: without it the application does not start.

  • Default: http://+:8080 in the Docker image

OTEL_EXPORTER_OTLP_ENDPOINT

Optional. When set, the OpenTelemetry data collected by the application is exported with the OTLP protocol to this endpoint. Usually passed as an environment variable.

  • Default: (none)
{
"CookieSettings": {
"ExpireDays": 7
},
"Security": {
"PasswordOptions": {
"RequiredLength": 12,
"RequireNonAlphanumeric": true,
"RequireDigit": true,
"RequireLowercase": true,
"RequireUppercase": true
},
"LockoutOptions": {
"MaxFailedAccessAttempts": 3,
"AllowedForNewUsers": true,
"DefaultLockoutTimeSpan": "00:30:00"
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "File",
"Args": {
"retainedFileCountLimit": 90
}
}
]
}
}

This example:

  • Shortens the login cookie to 7 days.
  • Makes passwords and lockout stricter.
  • Keeps 90 daily log files instead of 30. The File sink is the first element of WriteTo, the other sinks are untouched.