mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 20:16:35 +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
|
dockerfile: deploy/docker/Dockerfile.server
|
||||||
container_name: betterframe-server
|
container_name: betterframe-server
|
||||||
restart: unless-stopped
|
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:
|
volumes:
|
||||||
- betterframe-data:/var/lib/betterframe
|
- betterframe-data:/var/lib/betterframe
|
||||||
- ./sec-config.yaml:/app/server/sec-config.yaml:ro
|
- ./sec-config.yaml:/app/server/sec-config.yaml:ro
|
||||||
|
|
@ -26,6 +35,12 @@ services:
|
||||||
- "18080"
|
- "18080"
|
||||||
- "18081"
|
- "18081"
|
||||||
- "18082"
|
- "18082"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO- http://localhost:18080/healthz || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
networks:
|
networks:
|
||||||
- betterframe
|
- betterframe
|
||||||
|
|
||||||
|
|
@ -56,6 +71,12 @@ services:
|
||||||
- ./nodered-settings.js:/data/settings.js:ro
|
- ./nodered-settings.js:/data/settings.js:ro
|
||||||
expose:
|
expose:
|
||||||
- "1880"
|
- "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:
|
networks:
|
||||||
- betterframe
|
- betterframe
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import { initSecrets, type SecretsApi } from "../../shared/secrets.js";
|
||||||
import { createAuth, type AuthApi } from "../../shared/auth.js";
|
import { createAuth, type AuthApi } from "../../shared/auth.js";
|
||||||
import { initNoderedBridge, type NoderedBridge } from "../../shared/nodered-bridge.js";
|
import { initNoderedBridge, type NoderedBridge } from "../../shared/nodered-bridge.js";
|
||||||
import { initFirmware, type FirmwareApi } from "../../shared/firmware.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 type { Repository } from "../service-store/repository.js";
|
||||||
|
|
||||||
import { registerMiddleware } from "./middleware.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> {
|
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 repo = getRepo();
|
||||||
const secrets = initSecrets(
|
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, {}) },
|
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
||||||
);
|
);
|
||||||
const auth = createAuth(repo, secrets, {
|
const auth = createAuth(repo, secrets, {
|
||||||
|
|
@ -117,17 +125,17 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
||||||
argon2Memory: this.config.argon2Memory,
|
argon2Memory: this.config.argon2Memory,
|
||||||
argon2TimeCost: this.config.argon2TimeCost,
|
argon2TimeCost: this.config.argon2TimeCost,
|
||||||
argon2Parallelism: this.config.argon2Parallelism,
|
argon2Parallelism: this.config.argon2Parallelism,
|
||||||
totpIssuer: this.config.totpIssuer,
|
totpIssuer,
|
||||||
cookieName: this.config.cookieName,
|
cookieName,
|
||||||
});
|
});
|
||||||
|
|
||||||
const nodered = initNoderedBridge(
|
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, {}) },
|
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
||||||
);
|
);
|
||||||
|
|
||||||
const firmware = initFirmware(
|
const firmware = initFirmware(
|
||||||
{ dataDir: this.config.dataDir },
|
{ dataDir },
|
||||||
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
{ 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,
|
repo,
|
||||||
auth,
|
auth,
|
||||||
secrets,
|
secrets,
|
||||||
cookieName: this.config.cookieName,
|
cookieName,
|
||||||
nodered,
|
nodered,
|
||||||
firmware,
|
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
|
// 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
|
// to set server URL + API key manually. Best-effort with retries because
|
||||||
// Node-RED may still be starting.
|
// 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> {}
|
async run(_obs: Observable): Promise<void> {}
|
||||||
|
|
@ -224,6 +232,7 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
||||||
secrets: SecretsApi,
|
secrets: SecretsApi,
|
||||||
auth: AuthApi,
|
auth: AuthApi,
|
||||||
nodered: NoderedBridge,
|
nodered: NoderedBridge,
|
||||||
|
selfUrl: string,
|
||||||
obs: Observable,
|
obs: Observable,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let plaintext: string;
|
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]));
|
await new Promise((r) => setTimeout(r, delaysMs[attempt]));
|
||||||
obs.log.info("nodered: provisioning attempt {n} → {url}", {
|
obs.log.info("nodered: provisioning attempt {n} → {url}", {
|
||||||
n: attempt + 1,
|
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") {
|
if (result === "created") {
|
||||||
obs.log.info("nodered: provisioned bf-server-config at {url}", {
|
obs.log.info("nodered: provisioned bf-server-config at {url}", {
|
||||||
url: this.config.selfUrl,
|
url: selfUrl,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { initiatePairing, claimPairing } from "../../shared/pairing.js";
|
||||||
import { generateBundle } from "../../shared/bundle.js";
|
import { generateBundle } from "../../shared/bundle.js";
|
||||||
import { initNoderedBridge, type NoderedBridge } from "../../shared/nodered-bridge.js";
|
import { initNoderedBridge, type NoderedBridge } from "../../shared/nodered-bridge.js";
|
||||||
import { initFirmware, type FirmwareApi } from "../../shared/firmware.js";
|
import { initFirmware, type FirmwareApi } from "../../shared/firmware.js";
|
||||||
|
import { envStr } from "../../shared/env-overrides.js";
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import type { Repository } from "../service-store/repository.js";
|
import type { Repository } from "../service-store/repository.js";
|
||||||
import type { AuthApi } from "../../shared/auth.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> {
|
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 repo = getRepo();
|
||||||
const secrets = initSecrets(
|
const secrets = initSecrets(
|
||||||
{ dataDir: this.config.dataDir },
|
{ dataDir },
|
||||||
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
||||||
);
|
);
|
||||||
const auth = createAuth(repo, secrets, {
|
const auth = createAuth(repo, secrets, {
|
||||||
|
|
@ -100,16 +106,16 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
||||||
argon2Memory: this.config.argon2Memory,
|
argon2Memory: this.config.argon2Memory,
|
||||||
argon2TimeCost: this.config.argon2TimeCost,
|
argon2TimeCost: this.config.argon2TimeCost,
|
||||||
argon2Parallelism: this.config.argon2Parallelism,
|
argon2Parallelism: this.config.argon2Parallelism,
|
||||||
totpIssuer: this.config.totpIssuer,
|
totpIssuer,
|
||||||
cookieName: this.config.cookieName,
|
cookieName,
|
||||||
});
|
});
|
||||||
const codeTtl = this.config.codeTtlSeconds;
|
const codeTtl = this.config.codeTtlSeconds;
|
||||||
const nodered = initNoderedBridge(
|
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, {}) },
|
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
||||||
);
|
);
|
||||||
const firmware = initFirmware(
|
const firmware = initFirmware(
|
||||||
{ dataDir: this.config.dataDir },
|
{ dataDir },
|
||||||
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
{ 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 { createAuth } from "../../shared/auth.js";
|
||||||
import { setCoordinator } from "../../shared/coordinator-registry.js";
|
import { setCoordinator } from "../../shared/coordinator-registry.js";
|
||||||
import { initNoderedBridge, type NoderedBridge } from "../../shared/nodered-bridge.js";
|
import { initNoderedBridge, type NoderedBridge } from "../../shared/nodered-bridge.js";
|
||||||
|
import { envStr } from "../../shared/env-overrides.js";
|
||||||
|
|
||||||
// ---- Config -----------------------------------------------------------------
|
// ---- Config -----------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -112,13 +113,18 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(obs: Observable): Promise<void> {
|
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 repo = getRepo();
|
||||||
const secrets = initSecrets(
|
const secrets = initSecrets(
|
||||||
{ dataDir: this.config.dataDir },
|
{ dataDir },
|
||||||
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
||||||
);
|
);
|
||||||
const nodered = initNoderedBridge(
|
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, {}) },
|
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
||||||
);
|
);
|
||||||
this.nodered = nodered;
|
this.nodered = nodered;
|
||||||
|
|
@ -131,8 +137,8 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
||||||
argon2Memory: this.config.argon2Memory,
|
argon2Memory: this.config.argon2Memory,
|
||||||
argon2TimeCost: this.config.argon2TimeCost,
|
argon2TimeCost: this.config.argon2TimeCost,
|
||||||
argon2Parallelism: this.config.argon2Parallelism,
|
argon2Parallelism: this.config.argon2Parallelism,
|
||||||
totpIssuer: this.config.totpIssuer,
|
totpIssuer,
|
||||||
cookieName: this.config.cookieName,
|
cookieName,
|
||||||
});
|
});
|
||||||
|
|
||||||
const httpServer = createServer((req, res) => {
|
const httpServer = createServer((req, res) => {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import {
|
||||||
import { MIGRATIONS } from "./migrations.js";
|
import { MIGRATIONS } from "./migrations.js";
|
||||||
import { Repository } from "./repository.js";
|
import { Repository } from "./repository.js";
|
||||||
import { registerRepo } from "../../shared/plugin-registry.js";
|
import { registerRepo } from "../../shared/plugin-registry.js";
|
||||||
|
import { envStr } from "../../shared/env-overrides.js";
|
||||||
|
|
||||||
// ---- Config -----------------------------------------------------------------
|
// ---- Config -----------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -99,7 +100,7 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(obs: Observable): Promise<void> {
|
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 });
|
obs.log.info("opening sqlite at {path}", { path });
|
||||||
|
|
||||||
// Ensure parent dir exists (in dev BETTERFRAME_DATA_DIR may be in $HOME)
|
// 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