mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(deploy): follow PORT in every probe, wait for a slow boot, and probe the database
Changing PORT left the Dockerfile and compose healthchecks, the chart's containerPort, Service target and probes on 3000, so the container went unhealthy and Kubernetes restarted it in a loop. All of them follow PORT now. The chart gains a startup probe (ten minutes for a first boot, a long migration or a restore at boot, which liveness used to kill after about 105 s), a readiness probe on the new /api/health/ready that asks the database, extraEnv and extraEnvFrom for the settings the chart does not list, slots for the four secrets it had none for, and the DEFAULT_LANGUAGE and MCP_MAX_SESSION_PER_USER values it documented but never set. The Dockerfile no longer hides a failed npm ci behind a ';', pins gosu, and the language lists name all 27 locales.
This commit is contained in:
+5
-3
@@ -3,7 +3,9 @@
|
||||
# Go stdlib (Debian's apt gosu is built with an old Go that trips CVE scanners).
|
||||
# The binary and its runtime behaviour are identical to the apt package.
|
||||
FROM golang:1.25-alpine AS gosu-build
|
||||
RUN CGO_ENABLED=0 GOBIN=/out go install github.com/tianon/gosu@latest
|
||||
# Pinned to the commit of gosu's 1.19 tag: @latest made every build pick up whatever
|
||||
# was pushed last, and the tag itself is not a semver version go install accepts.
|
||||
RUN CGO_ENABLED=0 GOBIN=/out go install github.com/tianon/gosu@6456aaa0f3c854d199d0f037f068eb97515b7513
|
||||
|
||||
# ── Stage 1: shared ──────────────────────────────────────────────────────────
|
||||
FROM node:24-alpine AS shared-builder
|
||||
@@ -71,7 +73,7 @@ RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends tzdata dumb-init wget ca-certificates \
|
||||
libkitinerary-bin libsqlite3-0 && \
|
||||
npm ci --workspace=server --omit=dev --ignore-scripts && \
|
||||
ln -sf "$(find /usr/lib -name kitinerary-extractor -type f | head -1)" /usr/local/bin/kitinerary-extractor; \
|
||||
{ ln -sf "$(find /usr/lib -name kitinerary-extractor -type f | head -1)" /usr/local/bin/kitinerary-extractor || true; } && \
|
||||
rm -rf /var/lib/apt/lists/* /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx && \
|
||||
chown -R node:node /app
|
||||
|
||||
@@ -146,7 +148,7 @@ LABEL org.opencontainers.image.title="TREK" \
|
||||
EXPOSE 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||
CMD wget -qO- http://localhost:3000/api/health || exit 1
|
||||
CMD wget -qO- "http://localhost:${PORT:-3000}/api/health" || exit 1
|
||||
|
||||
# Start-up lives in a script, not an inline `sh -c` string: container management
|
||||
# UIs re-tokenise Config.Cmd when you edit a container in place, and a command
|
||||
|
||||
@@ -16,6 +16,12 @@ data:
|
||||
{{- if .Values.env.TREK_WIKI_DIR }}
|
||||
TREK_WIKI_DIR: {{ .Values.env.TREK_WIKI_DIR | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.env.DEFAULT_LANGUAGE }}
|
||||
DEFAULT_LANGUAGE: {{ .Values.env.DEFAULT_LANGUAGE | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.env.MCP_MAX_SESSION_PER_USER }}
|
||||
MCP_MAX_SESSION_PER_USER: {{ .Values.env.MCP_MAX_SESSION_PER_USER | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.env.ALLOWED_ORIGINS }}
|
||||
ALLOWED_ORIGINS: {{ .Values.env.ALLOWED_ORIGINS | quote }}
|
||||
{{- end }}
|
||||
|
||||
@@ -9,7 +9,7 @@ spec:
|
||||
# TREK is a single-writer SQLite app on a ReadWriteOnce PVC, so the default
|
||||
# RollingUpdate would start a second pod holding the same volume before the old one
|
||||
# exits — a Multi-Attach deadlock, or two processes on one travel.db. Recreate tears
|
||||
# the old pod down first. Override to RollingUpdate only with a ReadWriteMany volume.
|
||||
# the old pod down first. Keep it, see updateStrategy in values.yaml.
|
||||
strategy:
|
||||
type: {{ .Values.updateStrategy | default "Recreate" }}
|
||||
selector:
|
||||
@@ -40,10 +40,14 @@ spec:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
- name: http
|
||||
containerPort: {{ .Values.env.PORT | default 3000 }}
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "trek.fullname" . }}-config
|
||||
{{- with .Values.extraEnvFrom }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
- name: ENCRYPTION_KEY
|
||||
valueFrom:
|
||||
@@ -99,11 +103,42 @@ spec:
|
||||
name: {{ default (printf "%s-secret" (include "trek.fullname" .)) .Values.existingSecret }}
|
||||
key: VAPID_PRIVATE_KEY
|
||||
optional: true
|
||||
- name: SMTP_PASS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ default (printf "%s-secret" (include "trek.fullname" .)) .Values.existingSecret }}
|
||||
key: SMTP_PASS
|
||||
optional: true
|
||||
- name: MAPBOX_ACCESS_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ default (printf "%s-secret" (include "trek.fullname" .)) .Values.existingSecret }}
|
||||
key: MAPBOX_ACCESS_TOKEN
|
||||
optional: true
|
||||
- name: CARTO_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ default (printf "%s-secret" (include "trek.fullname" .)) .Values.existingSecret }}
|
||||
key: CARTO_API_KEY
|
||||
optional: true
|
||||
- name: DEMO_ADMIN_PASS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ default (printf "%s-secret" (include "trek.fullname" .)) .Values.existingSecret }}
|
||||
key: DEMO_ADMIN_PASS
|
||||
optional: true
|
||||
{{- with .Values.extraEnv }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /app/data
|
||||
- name: uploads
|
||||
mountPath: /app/uploads
|
||||
{{- with (default dict .Values.probes).startup }}
|
||||
startupProbe:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with (default dict .Values.probes).liveness }}
|
||||
livenessProbe:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
|
||||
@@ -32,6 +32,18 @@ data:
|
||||
{{- if .Values.secretEnv.VAPID_PRIVATE_KEY }}
|
||||
VAPID_PRIVATE_KEY: {{ .Values.secretEnv.VAPID_PRIVATE_KEY | b64enc | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.secretEnv.SMTP_PASS }}
|
||||
SMTP_PASS: {{ .Values.secretEnv.SMTP_PASS | b64enc | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.secretEnv.MAPBOX_ACCESS_TOKEN }}
|
||||
MAPBOX_ACCESS_TOKEN: {{ .Values.secretEnv.MAPBOX_ACCESS_TOKEN | b64enc | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.secretEnv.CARTO_API_KEY }}
|
||||
CARTO_API_KEY: {{ .Values.secretEnv.CARTO_API_KEY | b64enc | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.secretEnv.DEMO_ADMIN_PASS }}
|
||||
DEMO_ADMIN_PASS: {{ .Values.secretEnv.DEMO_ADMIN_PASS | b64enc | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if and (not .Values.existingSecret) (.Values.generateEncryptionKey) }}
|
||||
@@ -74,4 +86,16 @@ stringData:
|
||||
{{- if .Values.secretEnv.VAPID_PRIVATE_KEY }}
|
||||
VAPID_PRIVATE_KEY: {{ .Values.secretEnv.VAPID_PRIVATE_KEY | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.secretEnv.SMTP_PASS }}
|
||||
SMTP_PASS: {{ .Values.secretEnv.SMTP_PASS | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.secretEnv.MAPBOX_ACCESS_TOKEN }}
|
||||
MAPBOX_ACCESS_TOKEN: {{ .Values.secretEnv.MAPBOX_ACCESS_TOKEN | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.secretEnv.CARTO_API_KEY }}
|
||||
CARTO_API_KEY: {{ .Values.secretEnv.CARTO_API_KEY | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.secretEnv.DEMO_ADMIN_PASS }}
|
||||
DEMO_ADMIN_PASS: {{ .Values.secretEnv.DEMO_ADMIN_PASS | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -8,7 +8,7 @@ spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: 3000
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
|
||||
+37
-12
@@ -4,9 +4,10 @@ image:
|
||||
# tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# Deployment update strategy. Recreate is the safe default for the single-writer SQLite
|
||||
# DB on a ReadWriteOnce volume (the old pod is torn down before the new one starts).
|
||||
# Set to RollingUpdate only if you back the data volume with ReadWriteMany storage.
|
||||
# Deployment update strategy. Keep Recreate: TREK is a single-writer SQLite app, and
|
||||
# during a RollingUpdate the new pod migrates the database while the old one still
|
||||
# writes to it. A ReadWriteMany volume does not make that safe; SQLite's WAL needs both
|
||||
# processes on one host, which network filesystems do not provide.
|
||||
updateStrategy: Recreate
|
||||
|
||||
# Optional image pull secrets for private registries
|
||||
@@ -33,7 +34,7 @@ env:
|
||||
# DEFAULT_LANGUAGE: "en"
|
||||
# Default language on the login page for users with no saved preference.
|
||||
# Browser/OS language is auto-detected first; this is the fallback when no match is found.
|
||||
# Supported: de, en, es, fr, hu, nl, br, cs, pl, ru, zh, zh-TW, it, ar
|
||||
# Supported: ar, az, br, ca, cs, de, en, es, et, fr, gr, hu, id, it, ja, ko, nl, pl, ru, sk, sv, th, tr, uk, vi, zh, zh-TW
|
||||
# ALLOWED_ORIGINS: ""
|
||||
# NOTE: If using ingress, ensure env.ALLOWED_ORIGINS matches the domains in ingress.hosts for proper CORS configuration.
|
||||
# APP_URL: "https://trek.example.com"
|
||||
@@ -168,15 +169,19 @@ secretEnv:
|
||||
# Leave both empty to let TREK keep a generated pair in its database.
|
||||
VAPID_PRIVATE_KEY: ""
|
||||
|
||||
# If true, a random ENCRYPTION_KEY is generated at install and preserved across upgrades
|
||||
# If true, a random ENCRYPTION_KEY is generated at install and preserved across upgrades.
|
||||
# Only with helm install/upgrade against the cluster: under `helm template`, ArgoCD or Flux
|
||||
# the chart cannot read the existing secret and renders a new key every time. TREK then
|
||||
# refuses to start rather than lose its secrets; use existingSecret in those setups.
|
||||
generateEncryptionKey: false
|
||||
|
||||
# If set, the chart renders no Secret of its own and instead reads every secret env
|
||||
# var from this existing Kubernetes secret. Expected keys: ENCRYPTION_KEY (rename via
|
||||
# existingSecretKey), ADMIN_EMAIL, ADMIN_PASSWORD, OIDC_CLIENT_SECRET,
|
||||
# UNSPLASH_ACCESS_KEY, PLACES_API_KEY, AMAP_API_KEY, AMAP_API_SECRET,
|
||||
# VAPID_PRIVATE_KEY. Missing keys are simply left unset (they are optional), so only
|
||||
# provide the ones your setup needs.
|
||||
# VAPID_PRIVATE_KEY, SMTP_PASS, MAPBOX_ACCESS_TOKEN, CARTO_API_KEY, DEMO_ADMIN_PASS.
|
||||
# Missing keys are simply left unset (they are optional), so only provide the ones your
|
||||
# setup needs.
|
||||
existingSecret: ""
|
||||
existingSecretKey: ENCRYPTION_KEY
|
||||
|
||||
@@ -202,20 +207,40 @@ persistence:
|
||||
# httpGet handler. Switching handler means clearing the old one in the same override
|
||||
# (httpGet: null together with tcpSocket), otherwise the API rejects the pod with
|
||||
# "may not specify more than 1 handler type". Set probes.liveness to null to drop a probe.
|
||||
#
|
||||
# The startup probe gives a first boot, a long migration or a restore at boot up to ten
|
||||
# minutes before liveness takes over; liveness alone killed such a boot after about
|
||||
# 105 s, and the next start began it again. Readiness asks the database, so a pod whose
|
||||
# database does not answer stops getting traffic without being restarted.
|
||||
probes:
|
||||
startup:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
failureThreshold: 60
|
||||
liveness:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: 3000
|
||||
initialDelaySeconds: 15
|
||||
port: http
|
||||
periodSeconds: 30
|
||||
readiness:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
path: /api/health/ready
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
|
||||
# Further environment variables, for any setting the env block above does not list.
|
||||
# extraEnv:
|
||||
# - name: SESSION_DURATION
|
||||
# value: 7d
|
||||
extraEnv: []
|
||||
# Whole ConfigMaps or Secrets:
|
||||
# extraEnvFrom:
|
||||
# - secretRef:
|
||||
# name: trek-smtp
|
||||
extraEnvFrom: []
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
|
||||
+3
-2
@@ -22,7 +22,7 @@ services:
|
||||
- TZ=${TZ:-UTC} # Timezone for logs, reminders and scheduled tasks (e.g. Europe/Berlin)
|
||||
- LOG_LEVEL=${LOG_LEVEL:-info} # info = concise user actions; debug = verbose admin-level details
|
||||
# - TREK_WIKI_DIR=/app/wiki # Where the in-app Help pages (/help) read their content from. Leave unset: the image ships the wiki at /app/wiki and finds it automatically. Only set this to serve your own docs, mounted into the container. If the path does not exist, Help falls back to fetching the public GitHub wiki (needs outbound network, and tracks the latest release rather than your version).
|
||||
# - DEFAULT_LANGUAGE=en # Default language on the login page for users with no saved preference. Browser/OS language is auto-detected first; this is the fallback. Supported: de, en, es, fr, hu, nl, br, cs, pl, ru, zh, zh-TW, it, ar
|
||||
# - DEFAULT_LANGUAGE=en # Default language on the login page for users with no saved preference. Browser/OS language is auto-detected first; this is the fallback. Supported: ar, az, br, ca, cs, de, en, es, et, fr, gr, hu, id, it, ja, ko, nl, pl, ru, sk, sv, th, tr, uk, vi, zh, zh-TW
|
||||
# - SESSION_DURATION=30d # How long users stay logged in (trek_session JWT + cookie maxAge). Accepts: 1h | 12h | 7d | 30d | 90d. Default: 24h
|
||||
# - SESSION_DURATION_REMEMBER=30d # Session length when "Remember me" is ticked at login: longer-lived JWT + persistent cookie that survives browser restarts. Same format as SESSION_DURATION. Default: 30d
|
||||
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-} # Comma-separated origins for CORS and email notification links
|
||||
@@ -69,7 +69,8 @@ services:
|
||||
# - ./storage-config.json:/app/data/storage-config.json:ro # optional seed-once storage provisioning (wiki: Admin-Storage)
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"]
|
||||
# Through a shell so a changed PORT is followed.
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:$${PORT:-3000}/api/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
@@ -39,6 +39,12 @@ export class MaintenanceRepository {
|
||||
this.em.getContext();
|
||||
}
|
||||
|
||||
/** `SELECT 1`: whether the database answers at all, for the readiness probe. */
|
||||
async ping(): Promise<void> {
|
||||
this.validateRequestContext();
|
||||
await this.em.getConnection().execute('SELECT 1');
|
||||
}
|
||||
|
||||
/**
|
||||
* `PRAGMA wal_checkpoint(TRUNCATE)` — rendered text pinned, identical to
|
||||
* the legacy `db.exec('PRAGMA wal_checkpoint(TRUNCATE)')` it replaces at
|
||||
|
||||
@@ -348,7 +348,7 @@ export function applyGlobalMiddleware(
|
||||
|
||||
if (shouldForceHttps) {
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
if (req.path === '/api/health') return next();
|
||||
if (req.path === '/api/health' || req.path === '/api/health/ready') return next();
|
||||
if (req.secure || req.headers['x-forwarded-proto'] === 'https') return next();
|
||||
res.redirect(301, 'https://' + (configuredHost ?? req.headers.host) + req.url);
|
||||
});
|
||||
@@ -358,7 +358,7 @@ export function applyGlobalMiddleware(
|
||||
|
||||
// Request logging with sensitive field redaction (SENSITIVE_KEYS/redact above)
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
if (req.path === '/api/health') return next();
|
||||
if (req.path === '/api/health' || req.path === '/api/health/ready') return next();
|
||||
const startedAt = Date.now();
|
||||
res.on('finish', () => {
|
||||
const ms = Date.now() - startedAt;
|
||||
|
||||
@@ -114,8 +114,9 @@ export const PUBLIC_ROUTE_ALLOW_LIST: string[] = [
|
||||
// run. It never reads the request body as data.
|
||||
'DocSyncWebhookController.nudge',
|
||||
'FeaturesController.features',
|
||||
// The container/uptime probe.
|
||||
// The container/uptime probe, and the readiness probe beside it.
|
||||
'FeaturesController.health',
|
||||
'FeaturesController.ready',
|
||||
// Subscribable ICS feeds. The token in the path is the credential, and the
|
||||
// calendar client polling it has no TREK session to send.
|
||||
'FeedsPublicController.tripFeed',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Controller, Get, Res } from '@nestjs/common';
|
||||
import { EntityManager } from '@mikro-orm/core';
|
||||
import type { Response } from 'express';
|
||||
import { MaintenanceRepository } from '../../db/repositories/MaintenanceRepository';
|
||||
import { KitineraryExtractorService } from '../booking-import/kitinerary-extractor.service';
|
||||
import { AddonsService } from '../addons/addons.service';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
@@ -13,6 +15,7 @@ export class FeaturesController {
|
||||
constructor(
|
||||
private readonly extractor: KitineraryExtractorService,
|
||||
private readonly addons: AddonsService,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
/** The container/uptime probe. The forced-HTTPS redirect and HSTS exempt this
|
||||
@@ -24,6 +27,23 @@ export class FeaturesController {
|
||||
res.json({ status: 'ok' });
|
||||
}
|
||||
|
||||
/**
|
||||
* The readiness probe: 503 while the database does not answer (a restore
|
||||
* swapping it, a boot still migrating), so an orchestrator stops sending
|
||||
* traffic without killing the process. Liveness stays on the plain probe
|
||||
* above, which a long restore must not fail.
|
||||
*/
|
||||
@Get('ready')
|
||||
async ready(@Res() res: Response): Promise<void> {
|
||||
res.setHeader('Cache-Control', 'no-store, must-revalidate');
|
||||
try {
|
||||
await new MaintenanceRepository(this.em).ping();
|
||||
res.json({ status: 'ready' });
|
||||
} catch {
|
||||
res.status(503).json({ status: 'unavailable' });
|
||||
}
|
||||
}
|
||||
|
||||
@Get('features')
|
||||
async features() {
|
||||
return {
|
||||
|
||||
@@ -16,7 +16,7 @@ function make(available: boolean, aiEnabled: boolean) {
|
||||
return {
|
||||
extractor,
|
||||
addons,
|
||||
controller: new FeaturesController(extractor as Extractor as KitineraryExtractorService, addons as never),
|
||||
controller: new FeaturesController(extractor as Extractor as KitineraryExtractorService, addons as never, {} as never),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,4 +83,31 @@ describe('FeaturesController (GET /api/health/features)', () => {
|
||||
expect(res.headers['Cache-Control']).toBe('no-store, must-revalidate');
|
||||
expect(res.body).toEqual({ status: 'ok' });
|
||||
});
|
||||
|
||||
describe('the readiness probe (GET /api/health/ready)', () => {
|
||||
function response() {
|
||||
const res = { setHeader: vi.fn(), status: vi.fn(), json: vi.fn() };
|
||||
res.status.mockReturnValue(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
it('FEAT-010: answers ready while the database answers, and 503 when it does not', async () => {
|
||||
const execute = vi.fn().mockResolvedValue([]);
|
||||
const em = { getConnection: () => ({ execute }), getContext: () => em };
|
||||
const extractor = { isAvailable: vi.fn(() => true) };
|
||||
const controller = new FeaturesController(extractor as Extractor as KitineraryExtractorService, {} as never, em as never);
|
||||
|
||||
const ok = response();
|
||||
await controller.ready(ok as never);
|
||||
expect(execute).toHaveBeenCalledWith('SELECT 1');
|
||||
expect(ok.json).toHaveBeenCalledWith({ status: 'ready' });
|
||||
expect(ok.status).not.toHaveBeenCalled();
|
||||
|
||||
execute.mockRejectedValueOnce(new Error('connection is not available (restore in progress?)'));
|
||||
const down = response();
|
||||
await controller.ready(down as never);
|
||||
expect(down.status).toHaveBeenCalledWith(503);
|
||||
expect(down.json).toHaveBeenCalledWith({ status: 'unavailable' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@
|
||||
<Config Name="TZ" Target="TZ" Default="UTC" Mode="" Description="Timezone for logs, reminders and scheduled tasks (e.g. Europe/Berlin)." Type="Variable" Display="always" Required="false" Mask="false">UTC</Config>
|
||||
<Config Name="LOG_LEVEL" Target="LOG_LEVEL" Default="info" Mode="" Description="Log verbosity: info = concise user actions, debug = verbose admin-level details." Type="Variable" Display="advanced" Required="false" Mask="false">info</Config>
|
||||
<Config Name="TREK_WIKI_DIR" Target="TREK_WIKI_DIR" Default="" Mode="" Description="Where the in-app Help pages (/help) read their content from. Leave empty: the container ships the wiki at /app/wiki and finds it automatically. Only set this to serve your own docs from a mounted path. If the path does not exist, Help falls back to fetching the public GitHub wiki (needs outbound network, and tracks the latest release rather than your version)." Type="Variable" Display="advanced" Required="false" Mask="false"/>
|
||||
<Config Name="DEFAULT_LANGUAGE" Target="DEFAULT_LANGUAGE" Default="en" Mode="" Description="Default language shown on the login page for users with no saved preference. Browser/OS language is auto-detected first; this is the fallback when no match is found. Supported: de, en, es, fr, hu, nl, br, cs, pl, ru, zh, zh-TW, it, ar." Type="Variable" Display="advanced" Required="false" Mask="false">en</Config>
|
||||
<Config Name="DEFAULT_LANGUAGE" Target="DEFAULT_LANGUAGE" Default="en" Mode="" Description="Default language shown on the login page for users with no saved preference. Browser/OS language is auto-detected first; this is the fallback when no match is found. Supported: ar, az, br, ca, cs, de, en, es, et, fr, gr, hu, id, it, ja, ko, nl, pl, ru, sk, sv, th, tr, uk, vi, zh, zh-TW." Type="Variable" Display="advanced" Required="false" Mask="false">en</Config>
|
||||
<Config Name="ALLOWED_ORIGINS" Target="ALLOWED_ORIGINS" Default="" Mode="" Description="Comma-separated origins allowed for CORS and used as base URL in email notification links (e.g. https://trek.example.com)." Type="Variable" Display="always" Required="false" Mask="false"/>
|
||||
<Config Name="APP_URL" Target="APP_URL" Default="" Mode="" Description="Public base URL of this instance (e.g. https://trek.example.com). Required when OIDC is enabled — must match the redirect URI registered with your IdP. Also used as base URL for email notification links." Type="Variable" Display="always" Required="false" Mask="false"/>
|
||||
<Config Name="FORCE_HTTPS" Target="FORCE_HTTPS" Default="false" Mode="" Description="Optional. When true: HTTPS redirect, HSTS header, CSP upgrade-insecure-requests, and secure cookies. Only useful behind a TLS-terminating proxy. Requires TRUST_PROXY." Type="Variable" Display="advanced" Required="false" Mask="false">false</Config>
|
||||
|
||||
Reference in New Issue
Block a user