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
+12 -8
View File
@@ -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;
}
}
+10 -7
View File
@@ -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
+2 -1
View File
@@ -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
+36 -2
View File
@@ -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 `<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 { 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