Support _FILE env vars

Closes https://github.com/fosrl/docs-v2/issues/141
This commit is contained in:
Owen
2026-09-15 10:44:22 -04:00
parent 4b30911f06
commit c6c12f1dcb
5 changed files with 77 additions and 34 deletions
+13 -9
View File
@@ -1,5 +1,6 @@
import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres"; import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres";
import { readConfigFile } from "@server/lib/readConfigFile"; import { readConfigFile } from "@server/lib/readConfigFile";
import { readEnvOrFile } from "@server/lib/getEnvOrYaml";
import { withReplicas } from "drizzle-orm/pg-core"; import { withReplicas } from "drizzle-orm/pg-core";
import { createPool } from "./poolConfig"; import { createPool } from "./poolConfig";
@@ -7,17 +8,20 @@ function createDb() {
const config = readConfigFile(); const config = readConfigFile();
// check the environment variables for postgres config first before the config file // 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 = { config.postgres = {
connection_string: process.env.POSTGRES_CONNECTION_STRING connection_string: envConnectionString
}; };
if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) { const replicaConnectionStrings = readEnvOrFile(
const replicas = "POSTGRES_REPLICA_CONNECTION_STRINGS"
process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(",").map( );
(conn) => ({ if (replicaConnectionStrings) {
connection_string: conn.trim() const replicas = replicaConnectionStrings
}) .split(",")
); .map((conn) => ({
connection_string: conn.trim()
}));
config.postgres.replicas = replicas; config.postgres.replicas = replicas;
} }
} }
+11 -8
View File
@@ -1,5 +1,6 @@
import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres"; import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres";
import { readConfigFile } from "@server/lib/readConfigFile"; import { readConfigFile } from "@server/lib/readConfigFile";
import { readEnvOrFile } from "@server/lib/getEnvOrYaml";
import { withReplicas } from "drizzle-orm/pg-core"; import { withReplicas } from "drizzle-orm/pg-core";
import { build } from "@server/build"; import { build } from "@server/build";
import { db as mainDb } from "./driver"; import { db as mainDb } from "./driver";
@@ -17,7 +18,7 @@ function createLogsDb() {
const logsConfig = config.postgres_logs; const logsConfig = config.postgres_logs;
// Check environment variable first // 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 }> = []; let replicaConnections: Array<{ connection_string: string }> = [];
if (!connectionString && logsConfig) { if (!connectionString && logsConfig) {
@@ -26,13 +27,15 @@ function createLogsDb() {
} }
// If POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS is set, use it // If POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS is set, use it
if (process.env.POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS) { const replicaConnectionStrings = readEnvOrFile(
replicaConnections = "POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS"
process.env.POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS.split(",").map( );
(conn) => ({ if (replicaConnectionStrings) {
connection_string: conn.trim() replicaConnections = replicaConnectionStrings
}) .split(",")
); .map((conn) => ({
connection_string: conn.trim()
}));
} }
// If no logs database is configured, fall back to main database // If no logs database is configured, fall back to main database
+2 -1
View File
@@ -6,6 +6,7 @@ import fs from "fs";
import { APP_PATH } from "@server/lib/consts"; import { APP_PATH } from "@server/lib/consts";
import { existsSync, mkdirSync } from "fs"; import { existsSync, mkdirSync } from "fs";
import logger from "@server/logger"; import logger from "@server/logger";
import { readEnvOrFile } from "@server/lib/getEnvOrYaml";
export const location = path.join(APP_PATH, "db", "db.sqlite"); export const location = path.join(APP_PATH, "db", "db.sqlite");
export const exists = checkFileExists(location); export const exists = checkFileExists(location);
@@ -19,7 +20,7 @@ function createDb() {
: undefined; : undefined;
const sqlite = new Database(location, { verbose }); 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 // Enable WAL mode — allows concurrent readers + single writer, preventing
// contention across subsystems (verifySession, Traefik, audit, ping). // contention across subsystems (verifySession, Traefik, audit, ping).
// NOTE: journal_mode persists in the DB file once set; unsetting this // NOTE: journal_mode persists in the DB file once set; unsetting this
+36 -2
View File
@@ -1,3 +1,37 @@
export const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => { import fs from "fs";
return process.env[envVar] ?? valFromYaml;
// Resolves an environment variable, also honoring a `<envVar>_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;
};
+15 -14
View File
@@ -3,7 +3,7 @@ import * as yaml from "js-yaml";
import { configFilePath1, configFilePath2 } from "./consts"; import { configFilePath1, configFilePath2 } from "./consts";
import { z } from "zod"; import { z } from "zod";
import stoi from "./stoi"; import stoi from "./stoi";
import { getEnvOrYaml } from "./getEnvOrYaml"; import { getEnvOrYaml, readEnvOrFile } from "./getEnvOrYaml";
const portSchema = z.number().positive().gt(0).lte(65535); const portSchema = z.number().positive().gt(0).lte(65535);
@@ -160,14 +160,17 @@ export const configSchema = z
.boolean() .boolean()
.optional() .optional()
.default(false) .default(false)
.transform((val) => .transform((val) => {
process.env.ENABLE_AI_GATEWAY_CLIENT_IP_HEADER !== const envVal = readEnvOrFile(
undefined "ENABLE_AI_GATEWAY_CLIENT_IP_HEADER"
? process.env.ENABLE_AI_GATEWAY_CLIENT_IP_HEADER === );
"true" return envVal !== undefined ? envVal === "true" : val;
: val }),
), secret: z
secret: z.string().pipe(z.string().min(8)).optional(), .string()
.pipe(z.string().min(8))
.optional()
.transform(getEnvOrYaml("SERVER_SECRET")),
maxmind_db_path: z.string().optional(), maxmind_db_path: z.string().optional(),
maxmind_asn_path: z.string().optional() maxmind_asn_path: z.string().optional()
}) })
@@ -198,7 +201,8 @@ export const configSchema = z
dashboard_session_length_hours: 720, dashboard_session_length_hours: 720,
resource_session_length_hours: 720, resource_session_length_hours: 720,
trust_proxy: 1, trust_proxy: 1,
enable_ai_gateway_client_ip_header: false enable_ai_gateway_client_ip_header: false,
secret: undefined
}), }),
postgres: z postgres: z
.object({ .object({
@@ -499,10 +503,7 @@ export const configSchema = z
) )
.refine( .refine(
(data) => { (data) => {
// If hybrid is not defined, server secret must be defined. If its not defined already then pull it from env // If hybrid is not defined, server secret must be defined
if (data.server?.secret === undefined) {
data.server.secret = process.env.SERVER_SECRET;
}
return ( return (
data.server?.secret !== undefined && data.server?.secret !== undefined &&
data.server.secret.length > 0 data.server.secret.length > 0