From c6c12f1dcbfbfbfb8e7e6c95f2f7c79eb327c677 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 15 Sep 2026 10:44:22 -0400 Subject: [PATCH] Support _FILE env vars Closes https://github.com/fosrl/docs-v2/issues/141 --- server/db/pg/driver.ts | 22 ++++++++++++--------- server/db/pg/logsDriver.ts | 19 ++++++++++-------- server/db/sqlite/driver.ts | 3 ++- server/lib/getEnvOrYaml.ts | 38 ++++++++++++++++++++++++++++++++++-- server/lib/readConfigFile.ts | 29 ++++++++++++++------------- 5 files changed, 77 insertions(+), 34 deletions(-) diff --git a/server/db/pg/driver.ts b/server/db/pg/driver.ts index 9e07a6234..d657c4f9e 100644 --- a/server/db/pg/driver.ts +++ b/server/db/pg/driver.ts @@ -1,5 +1,6 @@ import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres"; import { readConfigFile } from "@server/lib/readConfigFile"; +import { readEnvOrFile } from "@server/lib/getEnvOrYaml"; import { withReplicas } from "drizzle-orm/pg-core"; import { createPool } from "./poolConfig"; @@ -7,17 +8,20 @@ function createDb() { const config = readConfigFile(); // check the environment variables for postgres config first before the config file - if (process.env.POSTGRES_CONNECTION_STRING) { + const envConnectionString = readEnvOrFile("POSTGRES_CONNECTION_STRING"); + if (envConnectionString) { config.postgres = { - connection_string: process.env.POSTGRES_CONNECTION_STRING + connection_string: envConnectionString }; - if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) { - const replicas = - process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(",").map( - (conn) => ({ - connection_string: conn.trim() - }) - ); + const replicaConnectionStrings = readEnvOrFile( + "POSTGRES_REPLICA_CONNECTION_STRINGS" + ); + if (replicaConnectionStrings) { + const replicas = replicaConnectionStrings + .split(",") + .map((conn) => ({ + connection_string: conn.trim() + })); config.postgres.replicas = replicas; } } diff --git a/server/db/pg/logsDriver.ts b/server/db/pg/logsDriver.ts index 2c34136de..701b4b11b 100644 --- a/server/db/pg/logsDriver.ts +++ b/server/db/pg/logsDriver.ts @@ -1,5 +1,6 @@ import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres"; import { readConfigFile } from "@server/lib/readConfigFile"; +import { readEnvOrFile } from "@server/lib/getEnvOrYaml"; import { withReplicas } from "drizzle-orm/pg-core"; import { build } from "@server/build"; import { db as mainDb } from "./driver"; @@ -17,7 +18,7 @@ function createLogsDb() { const logsConfig = config.postgres_logs; // Check environment variable first - let connectionString = process.env.POSTGRES_LOGS_CONNECTION_STRING; + let connectionString = readEnvOrFile("POSTGRES_LOGS_CONNECTION_STRING"); let replicaConnections: Array<{ connection_string: string }> = []; if (!connectionString && logsConfig) { @@ -26,13 +27,15 @@ function createLogsDb() { } // If POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS is set, use it - if (process.env.POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS) { - replicaConnections = - process.env.POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS.split(",").map( - (conn) => ({ - connection_string: conn.trim() - }) - ); + const replicaConnectionStrings = readEnvOrFile( + "POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS" + ); + if (replicaConnectionStrings) { + replicaConnections = replicaConnectionStrings + .split(",") + .map((conn) => ({ + connection_string: conn.trim() + })); } // If no logs database is configured, fall back to main database diff --git a/server/db/sqlite/driver.ts b/server/db/sqlite/driver.ts index a58ec1ead..d211e8278 100644 --- a/server/db/sqlite/driver.ts +++ b/server/db/sqlite/driver.ts @@ -6,6 +6,7 @@ import fs from "fs"; import { APP_PATH } from "@server/lib/consts"; import { existsSync, mkdirSync } from "fs"; import logger from "@server/logger"; +import { readEnvOrFile } from "@server/lib/getEnvOrYaml"; export const location = path.join(APP_PATH, "db", "db.sqlite"); export const exists = checkFileExists(location); @@ -19,7 +20,7 @@ function createDb() { : undefined; const sqlite = new Database(location, { verbose }); - if (process.env.ENABLE_SQLITE_WAL_MODE == "true") { + if (readEnvOrFile("ENABLE_SQLITE_WAL_MODE") == "true") { // Enable WAL mode — allows concurrent readers + single writer, preventing // contention across subsystems (verifySession, Traefik, audit, ping). // NOTE: journal_mode persists in the DB file once set; unsetting this diff --git a/server/lib/getEnvOrYaml.ts b/server/lib/getEnvOrYaml.ts index 62081cef9..13054fed3 100644 --- a/server/lib/getEnvOrYaml.ts +++ b/server/lib/getEnvOrYaml.ts @@ -1,3 +1,37 @@ -export const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => { - return process.env[envVar] ?? valFromYaml; +import fs from "fs"; + +// Resolves an environment variable, also honoring a `_FILE` variant +// that points to a file whose (trimmed) contents should be used as the +// value. This is the common convention for consuming Docker/Swarm secrets +// (e.g. mounted at /run/secrets/...) without putting the raw value in the +// container's environment. +export const readEnvOrFile = (envVar: string): string | undefined => { + const fileEnvVar = `${envVar}_FILE`; + const filePath = process.env[fileEnvVar]; + + if (filePath) { + if (process.env[envVar]) { + throw new Error( + `Both ${envVar} and ${fileEnvVar} are set. Please set only one.` + ); + } + + try { + return fs.readFileSync(filePath, "utf8").trim(); + } catch (error) { + throw new Error( + `Failed to read ${fileEnvVar} (${filePath}): ${ + error instanceof Error ? error.message : error + }` + ); + } + } + + return process.env[envVar]; }; + +export const getEnvOrYaml = + (envVar: string) => + (valFromYaml: string | undefined): string | undefined => { + return readEnvOrFile(envVar) ?? valFromYaml; + }; diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index d4f040b72..9f433054d 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -3,7 +3,7 @@ import * as yaml from "js-yaml"; import { configFilePath1, configFilePath2 } from "./consts"; import { z } from "zod"; import stoi from "./stoi"; -import { getEnvOrYaml } from "./getEnvOrYaml"; +import { getEnvOrYaml, readEnvOrFile } from "./getEnvOrYaml"; const portSchema = z.number().positive().gt(0).lte(65535); @@ -160,14 +160,17 @@ export const configSchema = z .boolean() .optional() .default(false) - .transform((val) => - process.env.ENABLE_AI_GATEWAY_CLIENT_IP_HEADER !== - undefined - ? process.env.ENABLE_AI_GATEWAY_CLIENT_IP_HEADER === - "true" - : val - ), - secret: z.string().pipe(z.string().min(8)).optional(), + .transform((val) => { + const envVal = readEnvOrFile( + "ENABLE_AI_GATEWAY_CLIENT_IP_HEADER" + ); + return envVal !== undefined ? envVal === "true" : val; + }), + secret: z + .string() + .pipe(z.string().min(8)) + .optional() + .transform(getEnvOrYaml("SERVER_SECRET")), maxmind_db_path: z.string().optional(), maxmind_asn_path: z.string().optional() }) @@ -198,7 +201,8 @@ export const configSchema = z dashboard_session_length_hours: 720, resource_session_length_hours: 720, trust_proxy: 1, - enable_ai_gateway_client_ip_header: false + enable_ai_gateway_client_ip_header: false, + secret: undefined }), postgres: z .object({ @@ -499,10 +503,7 @@ export const configSchema = z ) .refine( (data) => { - // If hybrid is not defined, server secret must be defined. If its not defined already then pull it from env - if (data.server?.secret === undefined) { - data.server.secret = process.env.SERVER_SECRET; - } + // If hybrid is not defined, server secret must be defined return ( data.server?.secret !== undefined && data.server.secret.length > 0