Configuration
Every setting lives in an environment variable using Nuxt's runtime-config convention:
NUXT_<KEY>— server-only, never sent to the browserNUXT_PUBLIC_<KEY>— exposed to the client
No rebuild is needed to change config — the app reads these at startup.
Configurator in the admin UI
Logged-in admins can view and export the running instance's env-var mapping at /admin/configurator — pre-filled values, one-click switch between docker-compose.yml, .env, and shell export formats, secrets redacted. See Admin panel → Configurator.
Required
| Variable | Description |
|---|---|
NUXT_DATABASE_URL | PostgreSQL: postgresql://… — SQLite: sqlite:./data.db or :memory: |
NUXT_ISSUER | OIDC issuer URL, must end with /oidc |
NUXT_APP_URL | Browser-reachable base URL |
NUXT_OIDC_CLIENTS_JSON | JSON array of OIDC client definitions (schema) |
NUXT_RP_ID | WebAuthn relying party domain (no scheme, no port) |
NUXT_RP_NAME | WebAuthn RP display name |
NUXT_RP_ORIGIN | WebAuthn origin URL, full scheme + port |
Optional — core
| Variable | Default | Description |
|---|---|---|
NUXT_JWT_SECRET | auto | ≥ 32 chars, HMAC for session JWTs. Auto-generated and persisted to data/jwt-secret.txt on first start. |
NUXT_TOTP_ENCRYPTION_KEY | auto | Encrypts authenticator-app (TOTP) secrets at rest. Auto-generated to data/totp-encryption-key.txt if unset; back it up with the data dir. Rotating it makes existing TOTP enrolments undecryptable (users must re-enrol). |
NUXT_JWT_PRIVATE_KEY | auto | RSA 2048 PKCS8 PEM. Auto-generated and persisted to data/jwt-private-key.pem. \n-escape for .env. |
NUXT_JWT_PRIVATE_KEY_PREVIOUS | (empty) | Retired signing key kept in the JWKS (verify-only) during a no-downtime key rotation. Same PEM format. Empty = single-key. |
NUXT_SESSION_MAX_AGE_MINUTES | 15 | How long a session survives without activity. The session JWT and its cookie always get the same lifetime, and the server re-issues both once half of it has elapsed — so an active user is never signed out mid-work, while an abandoned tab stops working after this long. 1–1440. |
NUXT_SESSION_REMEMBER_ME_MAX_AGE_DAYS | 7 | The same, for a login that ticked Remember me. 1–365. |
NUXT_TRUST_PROXY | false | Set true behind a reverse proxy that terminates TLS and sets X-Forwarded-*. The client IP is then read from the rightmost X-Forwarded-For entry (the hop your proxy recorded) — assumes a single trusted proxy. Off, the header is ignored entirely. |
NUXT_COOKIE_SECURE | auto | Defaults to true when NODE_ENV=production. Set false over plain HTTP for local dev. |
NUXT_REGISTRATION_ENABLED | true | Set false to disable new user signups. |
NUXT_AUTO_ADMIN_FIRST_USER | true | First registered user is promoted to admin. A bootstrapped admin already fills that slot. |
NUXT_BOOTSTRAP_ADMIN_EMAIL | (empty) | Create an admin with this address at startup if it has no account yet. The route to a first admin on a closed instance. See Admin panel → First-admin bootstrap. |
NUXT_BOOTSTRAP_ADMIN_PASSWORD | (empty) | Password for that admin. Empty = one is generated and logged once. Must satisfy the password policy or startup fails. Create-only: changing it after the account exists does nothing. |
NUXT_TWO_FACTOR_ENABLED | false | Enables email-code 2FA after password login. Passkeys and authenticator-app (TOTP) work as 2FA too. TOTP is per-user opt-in and needs no env var (it works even with this off / no SMTP). With this off, the emailed code is never accepted as a second factor even when SMTP is configured — a TOTP account can't be downgraded to mailbox access; the suspicious-login step-up is the one exception that still uses email. |
NUXT_MAGIC_LINK_ENABLED | true | Offer Email me a sign-in link on the login page: a single-use link, valid for 15 minutes, that signs the user in without a password. Exists only when SMTP is configured — without NUXT_SMTP_HOST it is off whatever this says. Accounts with TOTP still enter their code after the link. Set false if a mailbox alone must not open an account. See Security → Magic-link sign-in. |
NUXT_REQUIRE_TWO_FACTOR | false | Force every account to have a second factor. A user with no usable factor is corralled to /setup-2fa (hard server-enforced gate) until they enrol TOTP or a passkey. Any factor satisfies it — including the emailed code — so forced enrolment only triggers when email-2FA isn't available. See Security → Mandatory two-factor. |
NUXT_REQUIRE_TWO_FACTOR_SCOPE | all | Who the mandatory-2FA gate applies to: all or admins (only admin accounts). Ignored when require-2FA is off. |
NUXT_REQUIRE_TWO_FACTOR_GRACE_DAYS | 0 | New-account grace (days) before the gate applies, measured from account creation. 0 = gate immediately. Accounts older than N days are gated regardless (no bypass window when the policy is enabled). |
NUXT_SUSPICIOUS_LOGIN_ALERTS | false | Email the user a "new sign-in" alert on a login from a new IP or device. Opt-in; requires SMTP. IP accuracy behind a proxy needs NUXT_TRUST_PROXY. See Security → New-sign-in alerts. |
NUXT_SUSPICIOUS_LOGIN_STEP_UP | false | Force an email-code challenge before granting the session when a login is flagged suspicious (new IP/device). Opt-in; requires SMTP. Independent of the alert. See Security → New-sign-in alerts. |
NUXT_APP_ROLES_REQUIRE_GRANT | false | Restrict the app-roles API (GET and PUT /api/apps/roles): only read/write for users who already have an OIDC grant with the calling client (else 404, uniform with an unknown user) — same model as NUXT_APP_METADATA_REQUIRE_GRANT. See OIDC → App-managed roles. |
NUXT_APP_METADATA_ENABLED | false | Enable the per-user app metadata store (GET/PATCH /api/apps/metadata) — a small key→value store an app reads and writes with its own client_secret. When off both routes return 404; the admin panel still shows any rows already stored. See OIDC → Per-user app metadata. |
NUXT_APP_METADATA_REQUIRE_GRANT | false | Restrict the metadata API to users who already have an OIDC grant with the calling client (else 404, uniform with an unknown user). Applies to reads as well as writes. |
NUXT_OIDC_RESOURCE_AUDIENCE | (empty) | Comma-separated audience URIs (RFC 8707). Empty = all access tokens stay opaque. When set, a client that lists the audience in its allowedResources and requests resource=<audience> gets a JWT access token; audiences are deny-by-default. See OIDC → JWT access tokens. |
NUXT_OIDC_RESOURCE_SERVERS_JSON | (empty) | Richer JWT-AT resource config: JSON array of { audience, scopes?, accessTokenTTL?, gateClientId? } giving a resource its own scope set, a shorter token TTL (seconds) and/or a login role gate. Clients must name an audience in their allowedResources to request it. scopes accepts an array (["a","b"]) or the space-delimited string OAuth uses on the wire ("a b") — both mean the same. Merged with (overriding, per audience) NUXT_OIDC_RESOURCE_AUDIENCE. This is also how you bind a token to an MCP server — the audience becomes the token's aud. See OIDC → JWT access tokens. |
NUXT_BACKCHANNEL_LOGOUT_RETRY_DELAYS_MS | 2000,15000 | Comma-separated backoff delays (ms) before each back-channel-logout re-attempt after the inline first attempt. Empty = single attempt. Bounded + in-memory (lost on restart). |
NUXT_LOG_LEVEL | info | pino level: fatal error warn info debug trace. |
Connection pool
PostgreSQL only — SQLite is one file and always uses a single connection.
| Variable | Default | Description |
|---|---|---|
NUXT_DATABASE_POOL_MIN | 2 | Connections kept open while the instance is idle. 0 releases them all, so a quiet instance costs the server nothing. |
NUXT_DATABASE_POOL_MAX | 10 | Connections this instance may take at peak. |
max_connections on a PostgreSQL server is a shared budget, and the default is 100. Ten simultaneously busy instances at the default maximum exhaust it, and Postgres then refuses new connections server-wide — one instance's traffic spike becomes everyone's outage. If staging, production and another application point at the same cluster, give each a smaller ceiling:
NUXT_DATABASE_POOL_MIN=0
NUXT_DATABASE_POOL_MAX=3Both accept 0–1000 (1–1000 for the maximum); anything else — a word, a decimal, a negative — is rejected at startup with the variable named. An empty value (NUXT_DATABASE_POOL_MAX=) counts as unset and takes the default. That upper bound is a typo guard of ours, not a PostgreSQL limit: raise the maximum if a tuned cluster and a single dedicated instance call for it. A maximum below the minimum is refused by the pool itself: the process exits with Tarn: opt.max is smaller than opt.min before it serves a request.
Defaults are unchanged from earlier releases, so an instance that sets neither variable behaves exactly as it did before they existed.
SMTP (optional — email features degrade when absent)
If NUXT_SMTP_HOST is empty, email verification and 2FA are disabled, users are auto-verified on registration, and password reset via email is unavailable. Useful for local dev without Mailcatcher.
| Variable | Example |
|---|---|
NUXT_SMTP_HOST | smtp.sendgrid.net / mailcatcher |
NUXT_SMTP_PORT | 587 / 1025 |
NUXT_SMTP_SECURE | true for port 465 |
NUXT_SMTP_USER | apikey |
NUXT_SMTP_PASS | <api-key> |
NUXT_SMTP_FROM | Schleuse <noreply@…> |
NUXT_SMTP_REQUIRE_TLS | true (default) / auto / false |
NUXT_SMTP_TLS_REJECT_UNAUTHORIZED | true (default) |
NUXT_EMAIL_RETRY_DELAYS_MS | 30000,120000 (default) |
Set NUXT_SMTP_FROM. Left unset it falls back to noreply@localhost, a domain receivers cannot route or verify — so verification, password-reset and invite messages are likely to be rejected or spam-filed. Schleuse warns about it at startup. Both noreply@example.com and Schleuse <noreply@example.com> are accepted.
Transport security
NUXT_SMTP_REQUIRE_TLS decides what happens on a non-465 port:
| Value | Behaviour |
|---|---|
true (default) | Require STARTTLS and a valid certificate. No upgrade means no delivery. |
auto | Upgrade to STARTTLS whenever the relay advertises it. If the relay doesn't offer it — or rejects the command — delivery continues unencrypted. Never blocks mail, with one exception: when the relay advertises STARTTLS and then rejects it, the SMTP credentials are not sent over the plaintext session, so a relay that requires NUXT_SMTP_USER login refuses the mail. |
false | Never attempt STARTTLS, even when advertised. Legacy behaviour; only for a relay that mis-advertises it. |
Upgrading from 1.11.1 or earlier
The default was auto. If you never set this variable, the mail hop is now required to be encrypted, and there are two ways that can stop delivery — with two different fixes:
| Symptom | Cause | Fix |
|---|---|---|
| No upgrade, no delivery | the relay does not offer STARTTLS | NUXT_SMTP_REQUIRE_TLS=auto |
| Handshake fails | the relay offers STARTTLS but its certificate is self-signed, expired or from a private CA | NUXT_SMTP_TLS_REJECT_UNAUTHORIZED=false |
Reach for the second one first where it applies: it keeps STARTTLS mandatory and only stops verifying the certificate, where auto gives up on encryption altogether. A copied .env.example already pins auto, and the bundled docker-compose.yml pins it for the mailcatcher sidecar, so neither changes behaviour on upgrade.
Ignored when NUXT_SMTP_SECURE=true — implicit TLS encrypts the session from the first byte.
Certificates are validated only where TLS is mandatory (true, or implicit TLS). Under auto they deliberately are not: nodemailer cannot fall back from a failed TLS handshake, so checking there would stop mail for every self-signed relay rather than downgrading. That is the standard opportunistic-security posture (RFC 7435) — encryption without authentication still beats cleartext, and the admin page labels it honestly as certificate not verified.
NUXT_SMTP_TLS_REJECT_UNAUTHORIZED=false relaxes validation in mandatory mode, for a self-hosted relay with a self-signed certificate. The hop stays encrypted against passive capture but is no longer proof against an active man-in-the-middle. Only a literal false disables it — a typo keeps validation on.
Check what your deployment actually negotiated on the admin Email & SMTP page; it reports the measured result, not the configured intent.
NUXT_EMAIL_RETRY_DELAYS_MS controls automatic retry of verification, password-reset and resend emails: a comma-separated list of backoff delays (in milliseconds) before each re-attempt. The default 30000,120000 means three attempts — immediate, then +30 s, then +2 min. Set it to an empty string to disable retry (a single attempt). Retries are in-memory and not durable: pending retries are lost if the process restarts. Time-sensitive 2FA login codes are never retried — a failed code send surfaces immediately so the user can request a fresh one.
Password policy
| Variable | Default | Notes |
|---|---|---|
NUXT_PASSWORD_POLICY | standard | Presets: simple (6+ chars), standard (8+ with upper/lower/number/special), strong (12+ with all). A fixed 256-character maximum applies regardless of preset (argon2 input bound). |
NUXT_PASSWORD_MIN_LENGTH | from preset | Override individual rule |
NUXT_PASSWORD_REQUIRE_UPPERCASE | from preset | |
NUXT_PASSWORD_REQUIRE_LOWERCASE | from preset | |
NUXT_PASSWORD_REQUIRE_NUMBER | from preset | |
NUXT_PASSWORD_REQUIRE_SPECIAL | from preset | |
NUXT_PASSWORD_HISTORY_COUNT | 0 | Prevent reuse of the last N passwords on change/reset. 0 disables it. See below. |
Password reuse prevention
Set NUXT_PASSWORD_HISTORY_COUNT=N (an integer ≥ 1) to reject a password change or reset that reuses one of the user's last N passwords — the current password counts as the most recent of the N. For example, 3 blocks the current password and the two before it.
0disables the check entirely, including allowing a change back to the current password.- Default coupling: when
NUXT_PASSWORD_HISTORY_COUNTis left unset, thestrongpassword preset turns reuse prevention on (count3) automatically; other presets default to0. Setting the variable explicitly (including0) always wins. - Only hashed retired passwords are stored (in a
password_historytable, argon2id like the live credential); they are deleted with the account and are never part of the GDPR data export. - Increasing N takes effect going forward; previously pruned history is not recovered.
Rate limiting
Format: maxAttempts/windowMinutes. In-memory per-IP counters (resets on restart — acceptable for a single-instance sidecar).
| Variable | Default |
|---|---|
NUXT_RATE_LIMIT_LOGIN | 10/15 |
NUXT_RATE_LIMIT_REGISTER | 10/60 |
NUXT_RATE_LIMIT_PASSWORD_RESET | 5/60 |
CAPTCHA
Off by default. Set a provider and every unauthenticated form that sends mail to a typed-in address requires a solved challenge: sign-up (password and passkey) and forgot password. Sign-in is not gated — it sends no mail and has its own per-account limit. The check runs before anything that reveals whether an address is taken, so the "already registered" answer sits behind it too.
| Variable | Default | Description |
|---|---|---|
NUXT_PUBLIC_CAPTCHA_PROVIDER | (empty) | turnstile, recaptcha (v2 checkbox), hcaptcha or cap. Empty = off. |
NUXT_PUBLIC_CAPTCHA_SITE_KEY | (empty) | The provider's public site key. It is rendered into the page — public by design. |
NUXT_CAPTCHA_SECRET | (empty) | The provider's secret key. Never leaves the server. |
NUXT_PUBLIC_CAPTCHA_CAP_URL | (empty) | Cap only: base URL of your Cap instance, e.g. https://cap.example.com. |
Only one provider is active at a time, so the provider is a value, not part of the variable name. A provider without its site key and secret (or cap without a URL) refuses to start — verification fails closed, so a half-configured CAPTCHA would otherwise reject every sign-up.
The vendor providers need outbound HTTPS from Schleuse to the vendor, and fail closed without it — on an air-gapped deployment every sign-up would be rejected. Use cap there.
| Provider | Hosted by | Get keys |
|---|---|---|
turnstile | Cloudflare | Cloudflare dashboard → Turnstile |
recaptcha | reCAPTCHA admin console, v2 "I'm not a robot" keys — v3 (score) keys are refused. Free up to 10 000 checks a month — see the quota warning. | |
hcaptcha | Intuition Machines | hCaptcha dashboard |
cap | you | Cap Standalone → dashboard → New key |
Quick start with Cloudflare's test keys (always pass — swap them for real ones):
NUXT_PUBLIC_CAPTCHA_PROVIDER=turnstile
NUXT_PUBLIC_CAPTCHA_SITE_KEY=1x00000000000000000000AA
NUXT_CAPTCHA_SECRET=1x0000000000000000000000000000000AACap is the self-hosted, open-source option (proof of work, no tracking). How to run it is Cap's own documentation; Schleuse needs one thing from it: ENABLE_ASSETS_SERVER=true, because the widget script and its WASM solver are loaded from your instance (<cap-url>/assets/…), never from a CDN. The Cap URL must be reachable from your users' browsers and from Schleuse, which calls <cap-url>/<site-key>/siteverify.
Observability (metrics)
| Variable | Default | Notes |
|---|---|---|
NUXT_METRICS_TOKEN | (unset) | Bearer token that enables GET /metrics (Prometheus). Unset → the endpoint returns 404 (feature off). When set it must be ≥ 32 characters (startup fails otherwise), and every scrape must send Authorization: Bearer <token> (constant-time compared; never logged). Treated as a secret — the Configurator never echoes its value. Rotate by changing the env var and restarting. |
Provisioning API
| Variable | Default | Notes |
|---|---|---|
NUXT_ADMIN_API_TOKENS_JSON | (unset) | JSON array of bearer tokens for the provisioning API: [{"id":"migration-importer","token":"…"}]. Unset or [] → every /api/provisioning/** route returns 404 (feature off). Each token must be ≥ 32 characters and each id a lowercase slug ([a-z0-9_-]); duplicate ids or tokens fail startup. The id is not secret — it labels the caller in the audit log. Treated as a secret: the Configurator never echoes the value. |
The id exists so rotation needs no coordinated restart: add a second entry, deploy, move your importer across, then remove the first — both are accepted in between.
WARNING
This token can create sign-in-capable accounts. Keep the endpoints off the public internet where you can, and remove the token once a migration is finished. It cannot set is_admin. See Migrating users in.
Metrics are in-memory counters/histograms (reset on restart): login success/failure, registrations, 2FA challenges, rate-limit hits, an HTTP request-duration histogram, and standard Node/process metrics. See Deployment for the scrape config and exposure guidance.
Webhooks
| Variable | Default | Description |
|---|---|---|
NUXT_WEBHOOKS_JSON | (empty) | Outbound webhooks: a JSON array of { id, target, events, secret? }. Subscriptions name audit events — exactly (register), by prefix (admin.user.*) or all of them (*) — and an unknown name fails startup rather than never firing. With a secret (≥ 16 chars) every delivery is signed. Secrets are masked in the admin panel and in every export. See Webhooks. |
NUXT_WEBHOOK_RETRY_DELAYS_MS | (empty) | Comma-separated backoff delays (ms) before each re-attempt after the first. Empty = one attempt. In-memory and lost on restart — there is no delivery queue by design. |
Localization
See Multilingual for the full story.
| Variable | Default | Description |
|---|---|---|
NUXT_DEFAULT_LOCALE | en | BCP-47 tag. Must be listed in NUXT_SUPPORTED_LOCALES. |
NUXT_SUPPORTED_LOCALES | en | Comma-separated allow-list (e.g. en,de,fr,it). The UI ships en, de, fr, it; any other tag is email-only. |
Theme
All optional — defaults produce a dark neon-modern look. Variables use NUXT_PUBLIC_THEME_* and are exposed to the client. See the THEMES.md ready-to-use presets.
The theme also covers error pages: an unknown URL or an invalid / expired emailed link renders in the same layout, with a localised explanation and a way back to sign-in.
| Variable | Default | Notes |
|---|---|---|
NUXT_PUBLIC_THEME_NAME | Schleuse | App name shown in the UI and page titles. |
NUXT_PUBLIC_THEME_PRIMARY_COLOR | #06d6a0 | Primary hex colour; the full 50–950 palette is auto-derived via HSL. |
NUXT_PUBLIC_THEME_BACKGROUND_COLOR | #0a0e17 | Page background hex colour. |
NUXT_PUBLIC_THEME_TEXT_COLOR | #f1f5f9 | Base text hex colour. |
NUXT_PUBLIC_THEME_BORDER_RADIUS | 12px | Global border radius (CSS length). |
NUXT_PUBLIC_THEME_DARK_MODE | true | Whether the theme is dark-first (true | false). |
NUXT_PUBLIC_THEME_SURFACE_COLOR | (auto) | Card / surface background. Auto-derived when empty. |
NUXT_PUBLIC_THEME_SURFACE_SECONDARY_COLOR | (auto) | Secondary surface background. Auto-derived when empty. |
NUXT_PUBLIC_THEME_BORDER_COLOR | (auto) | Border colour. Auto-derived when empty. |
NUXT_PUBLIC_THEME_MUTED_COLOR | (auto) | Muted / secondary text colour. Auto-derived when empty. |
NUXT_PUBLIC_THEME_LOGO_URL | (empty) | URL or path to a logo shown on the login pages, in the admin sidebar and in email. Empty falls back to the bundled Schleuse mark — see the note below. |
NUXT_PUBLIC_THEME_BACKGROUND_IMAGE | (empty) | URL or path to a background image for the auth pages. |
NUXT_PUBLIC_THEME_BACKGROUND_LAYOUT | split | Auth-page layout with a background image: split (form left, image right) or full. |
Empty does not mean "no logo"
Leaving NUXT_PUBLIC_THEME_LOGO_URL empty shows the bundled Schleuse mark on the login pages, in the admin sidebar and as the browser-tab icon.
The one exception is deliberate: if you also set NUXT_PUBLIC_THEME_NAME to something other than Schleuse, the mark is not shown and your name is rendered as text instead. Schleuse will not put its own logo above your brand. To show your own mark, set NUXT_PUBLIC_THEME_LOGO_URL.
Email follows the same rule, including the fallback. A relative value is resolved against NUXT_APP_URL before it is sent, since a mail client has no origin of its own — but use a PNG or JPEG, because Gmail and Outlook render no SVG. See Email templates.
Public UI flags
Also NUXT_PUBLIC_* (exposed to the client), but not part of the colour theme:
| Variable | Default | Notes |
|---|---|---|
NUXT_PUBLIC_CUSTOM_DISPLAY_NAME | false | Allow users to set a custom display name. When false, it is auto-composed from first + last name. |
NUXT_PUBLIC_HOME_REDIRECT_URL | (empty) | When set, visitors to / are redirected here (internal path or external URL), and the logo on the account page links here too. |
NUXT_PUBLIC_PASSWORD_HINT | (empty) | Override the auto-generated password-complexity hint shown under password fields. |
JWT keys
Both NUXT_JWT_SECRET and NUXT_JWT_PRIVATE_KEY are auto-generated on first start when not set. Generated values are persisted to the data/ directory:
| File | Contents | Purpose |
|---|---|---|
data/jwt-secret.txt | 48-byte random base64url | HMAC for session JWTs |
data/jwt-private-key.pem | RSA 2048 PKCS8 PEM | OIDC ID-token signing |
data/totp-encryption-key.txt | 48-byte random base64url | Encrypts TOTP secrets at rest |
Files are created with mode 0600. Mount /app/data as a Docker volume to persist keys across container restarts.
Priority: ENV variable > persisted file > generate new.
To generate keys manually:
# JWT secret (≥ 32 chars)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# RSA private key (newlines escaped for .env)
node -e "
const {generateKeyPairSync} = require('crypto');
const {privateKey} = generateKeyPairSync('rsa', {modulusLength: 2048});
const pem = privateKey.export({type:'pkcs8',format:'pem'});
console.log(pem.replace(/\n/g,'\\\\n'));
"Startup validation
The app validates the environment once at startup and refuses to boot on contradictory configuration — so operator typos surface immediately in the logs instead of as a mysterious outage months later. Fatal checks (the process exits with a clear level: fatal log naming the variable):
NUXT_ISSUERmust end with/oidc— the OIDC endpoints (discovery, authorize, token, jwks) are mounted there.NUXT_COOKIE_SECURE=truetogether with anhttp://NUXT_APP_URL— secure cookies are never sent over plain HTTP, so no session would survive. Use HTTPS, or setNUXT_COOKIE_SECURE=falsefor local HTTP.
Non-fatal (logged as level: warn, app still starts):
NUXT_APP_URLandNUXT_RP_ORIGINresolving to different origins — WebAuthn rejects assertions whose origin doesn't match the relying-party origin. Make them match unless the split is intentional.
These are in addition to the per-variable validation (URL shape, required values, secret length, etc.). If the container exits on boot, check the startup logs for a fatal line.