mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 16:56:33 +00:00
feat(server): env-var overrides for sec-config keys + docker healthchecks
This commit is contained in:
parent
69cd0391b5
commit
d1fd128ea0
6 changed files with 90 additions and 21 deletions
|
|
@ -19,6 +19,15 @@ services:
|
|||
dockerfile: deploy/docker/Dockerfile.server
|
||||
container_name: betterframe-server
|
||||
restart: unless-stopped
|
||||
# Env overrides win over sec-config.yaml — Coolify / k8s inject these.
|
||||
environment:
|
||||
- TZ=UTC
|
||||
- BF_DATA_DIR=/var/lib/betterframe
|
||||
- BF_SQLITE_PATH=/var/lib/betterframe/betterframe.db
|
||||
- BF_NODERED_URL=http://nodered:1880
|
||||
- BF_SELF_URL=http://server:18080
|
||||
# Optional: paste Ed25519 PEM private key here for firmware signing.
|
||||
# - BF_FIRMWARE_SIGNING_KEY=
|
||||
volumes:
|
||||
- betterframe-data:/var/lib/betterframe
|
||||
- ./sec-config.yaml:/app/server/sec-config.yaml:ro
|
||||
|
|
@ -26,6 +35,12 @@ services:
|
|||
- "18080"
|
||||
- "18081"
|
||||
- "18082"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:18080/healthz || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
networks:
|
||||
- betterframe
|
||||
|
||||
|
|
@ -56,6 +71,12 @@ services:
|
|||
- ./nodered-settings.js:/data/settings.js:ro
|
||||
expose:
|
||||
- "1880"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:1880/nrdp/auth/login >/dev/null || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
networks:
|
||||
- betterframe
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { initSecrets, type SecretsApi } from "../../shared/secrets.js";
|
|||
import { createAuth, type AuthApi } from "../../shared/auth.js";
|
||||
import { initNoderedBridge, type NoderedBridge } from "../../shared/nodered-bridge.js";
|
||||
import { initFirmware, type FirmwareApi } from "../../shared/firmware.js";
|
||||
import { envStr } from "../../shared/env-overrides.js";
|
||||
import type { Repository } from "../service-store/repository.js";
|
||||
|
||||
import { registerMiddleware } from "./middleware.js";
|
||||
|
|
@ -103,10 +104,17 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
|||
}
|
||||
|
||||
async init(obs: Observable): Promise<void> {
|
||||
// Init shared modules — no inter-plugin wiring needed
|
||||
// Init shared modules — no inter-plugin wiring needed.
|
||||
// Env-var overrides for Coolify / 12-factor deploys (BF_* prefix).
|
||||
const dataDir = envStr("BF_DATA_DIR", this.config.dataDir);
|
||||
const noderedUrl = envStr("BF_NODERED_URL", this.config.noderedUrl);
|
||||
const selfUrl = envStr("BF_SELF_URL", this.config.selfUrl);
|
||||
const cookieName = envStr("BF_COOKIE_NAME", this.config.cookieName);
|
||||
const totpIssuer = envStr("BF_TOTP_ISSUER", this.config.totpIssuer);
|
||||
|
||||
const repo = getRepo();
|
||||
const secrets = initSecrets(
|
||||
{ dataDir: this.config.dataDir, systemdCredsName: this.config.systemdCredsName },
|
||||
{ dataDir, systemdCredsName: this.config.systemdCredsName },
|
||||
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
||||
);
|
||||
const auth = createAuth(repo, secrets, {
|
||||
|
|
@ -117,17 +125,17 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
|||
argon2Memory: this.config.argon2Memory,
|
||||
argon2TimeCost: this.config.argon2TimeCost,
|
||||
argon2Parallelism: this.config.argon2Parallelism,
|
||||
totpIssuer: this.config.totpIssuer,
|
||||
cookieName: this.config.cookieName,
|
||||
totpIssuer,
|
||||
cookieName,
|
||||
});
|
||||
|
||||
const nodered = initNoderedBridge(
|
||||
{ baseUrl: this.config.noderedUrl },
|
||||
{ baseUrl: noderedUrl },
|
||||
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
||||
);
|
||||
|
||||
const firmware = initFirmware(
|
||||
{ dataDir: this.config.dataDir },
|
||||
{ dataDir },
|
||||
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
||||
);
|
||||
|
||||
|
|
@ -135,7 +143,7 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
|||
repo,
|
||||
auth,
|
||||
secrets,
|
||||
cookieName: this.config.cookieName,
|
||||
cookieName,
|
||||
nodered,
|
||||
firmware,
|
||||
};
|
||||
|
|
@ -214,7 +222,7 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
|||
// Auto-provision the Node-RED bf-server-config so the user doesn't have
|
||||
// to set server URL + API key manually. Best-effort with retries because
|
||||
// Node-RED may still be starting.
|
||||
void this.provisionNoderedBridge(repo, secrets, auth, nodered, obs);
|
||||
void this.provisionNoderedBridge(repo, secrets, auth, nodered, selfUrl, obs);
|
||||
}
|
||||
|
||||
async run(_obs: Observable): Promise<void> {}
|
||||
|
|
@ -224,6 +232,7 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
|||
secrets: SecretsApi,
|
||||
auth: AuthApi,
|
||||
nodered: NoderedBridge,
|
||||
selfUrl: string,
|
||||
obs: Observable,
|
||||
): Promise<void> {
|
||||
let plaintext: string;
|
||||
|
|
@ -241,12 +250,12 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
|||
await new Promise((r) => setTimeout(r, delaysMs[attempt]));
|
||||
obs.log.info("nodered: provisioning attempt {n} → {url}", {
|
||||
n: attempt + 1,
|
||||
url: this.config.selfUrl,
|
||||
url: selfUrl,
|
||||
});
|
||||
const result = await nodered.ensureServerConfig(this.config.selfUrl, plaintext);
|
||||
const result = await nodered.ensureServerConfig(selfUrl, plaintext);
|
||||
if (result === "created") {
|
||||
obs.log.info("nodered: provisioned bf-server-config at {url}", {
|
||||
url: this.config.selfUrl,
|
||||
url: selfUrl,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { initiatePairing, claimPairing } from "../../shared/pairing.js";
|
|||
import { generateBundle } from "../../shared/bundle.js";
|
||||
import { initNoderedBridge, type NoderedBridge } from "../../shared/nodered-bridge.js";
|
||||
import { initFirmware, type FirmwareApi } from "../../shared/firmware.js";
|
||||
import { envStr } from "../../shared/env-overrides.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import type { Repository } from "../service-store/repository.js";
|
||||
import type { AuthApi } from "../../shared/auth.js";
|
||||
|
|
@ -87,9 +88,14 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
|||
}
|
||||
|
||||
async init(obs: Observable): Promise<void> {
|
||||
const dataDir = envStr("BF_DATA_DIR", this.config.dataDir);
|
||||
const noderedUrl = envStr("BF_NODERED_URL", this.config.noderedUrl);
|
||||
const cookieName = envStr("BF_COOKIE_NAME", this.config.cookieName);
|
||||
const totpIssuer = envStr("BF_TOTP_ISSUER", this.config.totpIssuer);
|
||||
|
||||
const repo = getRepo();
|
||||
const secrets = initSecrets(
|
||||
{ dataDir: this.config.dataDir },
|
||||
{ dataDir },
|
||||
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
||||
);
|
||||
const auth = createAuth(repo, secrets, {
|
||||
|
|
@ -100,16 +106,16 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
|||
argon2Memory: this.config.argon2Memory,
|
||||
argon2TimeCost: this.config.argon2TimeCost,
|
||||
argon2Parallelism: this.config.argon2Parallelism,
|
||||
totpIssuer: this.config.totpIssuer,
|
||||
cookieName: this.config.cookieName,
|
||||
totpIssuer,
|
||||
cookieName,
|
||||
});
|
||||
const codeTtl = this.config.codeTtlSeconds;
|
||||
const nodered = initNoderedBridge(
|
||||
{ baseUrl: this.config.noderedUrl },
|
||||
{ baseUrl: noderedUrl },
|
||||
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
||||
);
|
||||
const firmware = initFirmware(
|
||||
{ dataDir: this.config.dataDir },
|
||||
{ dataDir },
|
||||
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { initSecrets } from "../../shared/secrets.js";
|
|||
import { createAuth } from "../../shared/auth.js";
|
||||
import { setCoordinator } from "../../shared/coordinator-registry.js";
|
||||
import { initNoderedBridge, type NoderedBridge } from "../../shared/nodered-bridge.js";
|
||||
import { envStr } from "../../shared/env-overrides.js";
|
||||
|
||||
// ---- Config -----------------------------------------------------------------
|
||||
|
||||
|
|
@ -112,13 +113,18 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
|||
}
|
||||
|
||||
async init(obs: Observable): Promise<void> {
|
||||
const dataDir = envStr("BF_DATA_DIR", this.config.dataDir);
|
||||
const noderedUrl = envStr("BF_NODERED_URL", this.config.noderedUrl);
|
||||
const cookieName = envStr("BF_COOKIE_NAME", this.config.cookieName);
|
||||
const totpIssuer = envStr("BF_TOTP_ISSUER", this.config.totpIssuer);
|
||||
|
||||
const repo = getRepo();
|
||||
const secrets = initSecrets(
|
||||
{ dataDir: this.config.dataDir },
|
||||
{ dataDir },
|
||||
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
||||
);
|
||||
const nodered = initNoderedBridge(
|
||||
{ baseUrl: this.config.noderedUrl },
|
||||
{ baseUrl: noderedUrl },
|
||||
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
||||
);
|
||||
this.nodered = nodered;
|
||||
|
|
@ -131,8 +137,8 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
|||
argon2Memory: this.config.argon2Memory,
|
||||
argon2TimeCost: this.config.argon2TimeCost,
|
||||
argon2Parallelism: this.config.argon2Parallelism,
|
||||
totpIssuer: this.config.totpIssuer,
|
||||
cookieName: this.config.cookieName,
|
||||
totpIssuer,
|
||||
cookieName,
|
||||
});
|
||||
|
||||
const httpServer = createServer((req, res) => {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import {
|
|||
import { MIGRATIONS } from "./migrations.js";
|
||||
import { Repository } from "./repository.js";
|
||||
import { registerRepo } from "../../shared/plugin-registry.js";
|
||||
import { envStr } from "../../shared/env-overrides.js";
|
||||
|
||||
// ---- Config -----------------------------------------------------------------
|
||||
|
||||
|
|
@ -99,7 +100,7 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
|||
}
|
||||
|
||||
async init(obs: Observable): Promise<void> {
|
||||
const path = this.config.sqlitePath;
|
||||
const path = envStr("BF_SQLITE_PATH", this.config.sqlitePath);
|
||||
obs.log.info("opening sqlite at {path}", { path });
|
||||
|
||||
// Ensure parent dir exists (in dev BETTERFRAME_DATA_DIR may be in $HOME)
|
||||
|
|
|
|||
26
server/src/shared/env-overrides.ts
Normal file
26
server/src/shared/env-overrides.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* Tiny env-override helpers for Coolify / 12-factor deploys.
|
||||
*
|
||||
* sec-config.yaml stays the single declarative source of truth, but a handful
|
||||
* of values benefit from runtime env-var injection (URL of the upstream
|
||||
* Node-RED, the BF server's own public URL, paths to data dirs, etc.). These
|
||||
* helpers are called inline from each plugin's init() so a missing env var
|
||||
* simply falls back to the YAML value.
|
||||
*/
|
||||
export function envStr(key: string, fallback: string): string {
|
||||
const v = process.env[key];
|
||||
return v && v.length > 0 ? v : fallback;
|
||||
}
|
||||
|
||||
export function envInt(key: string, fallback: number): number {
|
||||
const v = process.env[key];
|
||||
if (!v) return fallback;
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
export function envBool(key: string, fallback: boolean): boolean {
|
||||
const v = process.env[key]?.toLowerCase();
|
||||
if (v === undefined) return fallback;
|
||||
return v === "1" || v === "true" || v === "yes" || v === "on";
|
||||
}
|
||||
Loading…
Reference in a new issue