diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index 73998e4..2b4de57 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -77,8 +77,25 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Check if version image already exists
+ id: existence
+ shell: bash
+ run: |
+ set -euo pipefail
+ image="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
+ version="${{ steps.srcver.outputs.version }}"
+
+ if docker buildx imagetools inspect "${image}:${version}" >/dev/null 2>&1; then
+ echo "Image already exists for version ${version}; skipping publish."
+ echo "should_push=false" >> "$GITHUB_OUTPUT"
+ else
+ echo "No image found for version ${version}; publishing enabled."
+ echo "should_push=true" >> "$GITHUB_OUTPUT"
+ fi
+
- name: Extract metadata
id: meta
+ if: steps.existence.outputs.should_push == 'true'
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
@@ -89,6 +106,7 @@ jobs:
type=raw,value=latest
- name: Build and push server image
+ if: steps.existence.outputs.should_push == 'true'
uses: docker/build-push-action@v5
with:
context: .
@@ -99,3 +117,7 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
+
+ - name: Publish skipped
+ if: steps.existence.outputs.should_push != 'true'
+ run: echo "Docker image push skipped because this version already exists in GHCR."
diff --git a/.gitignore b/.gitignore
index f50f955..31a2f0b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,4 +45,6 @@ plugin-sample-go/*.zip
Overlord-Server/data/
# Release packaging output
-release/
\ No newline at end of file
+release/
+
+stress/
\ No newline at end of file
diff --git a/Overlord-Server/config.json.example b/Overlord-Server/config.json.example
index dbcf4ac..565aeee 100644
--- a/Overlord-Server/config.json.example
+++ b/Overlord-Server/config.json.example
@@ -11,7 +11,15 @@
"tls": {
"certPath": "./certs/server.crt",
"keyPath": "./certs/server.key",
- "caPath": ""
+ "caPath": "",
+ "certbot": {
+ "enabled": false,
+ "livePath": "/etc/letsencrypt/live",
+ "domain": "",
+ "certFileName": "fullchain.pem",
+ "keyFileName": "privkey.pem",
+ "caFileName": "chain.pem"
+ }
},
"notifications": {
"keywords": ["bank", "password", "admin"],
diff --git a/Overlord-Server/public/assets/settings.js b/Overlord-Server/public/assets/settings.js
index b4ae948..5fab0e0 100644
--- a/Overlord-Server/public/assets/settings.js
+++ b/Overlord-Server/public/assets/settings.js
@@ -36,8 +36,19 @@ const securityRequireLowercaseInput = document.getElementById("security-require-
const securityRequireNumberInput = document.getElementById("security-require-number");
const securityRequireSymbolInput = document.getElementById("security-require-symbol");
+const tlsForm = document.getElementById("tls-form");
+const tlsPermissionNote = document.getElementById("tls-permission-note");
+const tlsSaveBtn = document.getElementById("tls-save-btn");
+const tlsCertbotEnabledInput = document.getElementById("tls-certbot-enabled");
+const tlsCertbotLivePathInput = document.getElementById("tls-certbot-live-path");
+const tlsCertbotDomainInput = document.getElementById("tls-certbot-domain");
+const tlsCertbotCertFileInput = document.getElementById("tls-certbot-cert-file");
+const tlsCertbotKeyFileInput = document.getElementById("tls-certbot-key-file");
+const tlsCertbotCaFileInput = document.getElementById("tls-certbot-ca-file");
+
let currentUser = null;
let securityConfig = null;
+let tlsConfig = null;
function showMessage(text, type = "ok") {
if (!messageEl) return;
@@ -123,6 +134,23 @@ function setSecurityFormDisabled(disabled) {
}
}
+function setTlsFormDisabled(disabled) {
+ const controls = [
+ tlsCertbotEnabledInput,
+ tlsCertbotLivePathInput,
+ tlsCertbotDomainInput,
+ tlsCertbotCertFileInput,
+ tlsCertbotKeyFileInput,
+ tlsCertbotCaFileInput,
+ tlsSaveBtn,
+ ];
+
+ for (const control of controls) {
+ if (!control) continue;
+ control.disabled = disabled;
+ }
+}
+
function applySecurityForm() {
if (!securityConfig) return;
securitySessionTtlInput.value = String(securityConfig.sessionTtlHours || 168);
@@ -169,6 +197,50 @@ async function loadSecurityPolicy() {
setSecurityFormDisabled(false);
}
+function applyTlsForm() {
+ const certbot = tlsConfig?.certbot || {};
+ tlsCertbotEnabledInput.checked = Boolean(certbot.enabled);
+ tlsCertbotLivePathInput.value = certbot.livePath || "/etc/letsencrypt/live";
+ tlsCertbotDomainInput.value = certbot.domain || "";
+ tlsCertbotCertFileInput.value = certbot.certFileName || "fullchain.pem";
+ tlsCertbotKeyFileInput.value = certbot.keyFileName || "privkey.pem";
+ tlsCertbotCaFileInput.value = certbot.caFileName || "chain.pem";
+}
+
+async function loadTlsSettings() {
+ if (!currentUser) return;
+
+ if (!isAdmin(currentUser.role)) {
+ tlsPermissionNote.classList.remove("hidden");
+ setTlsFormDisabled(true);
+ tlsConfig = {
+ certbot: {
+ enabled: false,
+ livePath: "/etc/letsencrypt/live",
+ domain: "",
+ certFileName: "fullchain.pem",
+ keyFileName: "privkey.pem",
+ caFileName: "chain.pem",
+ },
+ };
+ applyTlsForm();
+ return;
+ }
+
+ tlsPermissionNote.classList.add("hidden");
+ const res = await fetch("/api/settings/tls", { credentials: "include" });
+ if (!res.ok) {
+ showMessage("Failed to load TLS settings.", "error");
+ setTlsFormDisabled(true);
+ return;
+ }
+
+ const data = await res.json().catch(() => ({}));
+ tlsConfig = data.tls || null;
+ applyTlsForm();
+ setTlsFormDisabled(false);
+}
+
function loadPrefs() {
prefNotificationsInput.checked = getNotificationsEnabled();
const refreshSeconds = Number(localStorage.getItem(PREF_REFRESH_KEY) || 8);
@@ -277,6 +349,42 @@ async function saveSecurityPolicy(event) {
showMessage("Security policy updated.");
}
+async function saveTlsSettings(event) {
+ event.preventDefault();
+ if (!isAdmin(currentUser?.role)) {
+ showMessage("Admin role required.", "error");
+ return;
+ }
+
+ const payload = {
+ certbot: {
+ enabled: tlsCertbotEnabledInput.checked,
+ livePath: String(tlsCertbotLivePathInput.value || "").trim(),
+ domain: String(tlsCertbotDomainInput.value || "").trim(),
+ certFileName: String(tlsCertbotCertFileInput.value || "").trim(),
+ keyFileName: String(tlsCertbotKeyFileInput.value || "").trim(),
+ caFileName: String(tlsCertbotCaFileInput.value || "").trim(),
+ },
+ };
+
+ const res = await fetch("/api/settings/tls", {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ credentials: "include",
+ body: JSON.stringify(payload),
+ });
+
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok) {
+ showMessage(data.error || "Failed to save TLS settings.", "error");
+ return;
+ }
+
+ tlsConfig = data.tls || payload;
+ applyTlsForm();
+ showMessage("TLS settings updated. Restart server to apply.");
+}
+
async function loadBannedIps() {
if (!currentUser) return;
@@ -368,11 +476,13 @@ async function init() {
await loadCurrentUser();
loadPrefs();
await loadSecurityPolicy();
+ await loadTlsSettings();
await loadBannedIps();
passwordForm.addEventListener("submit", updatePassword);
prefsForm.addEventListener("submit", savePrefs);
securityForm.addEventListener("submit", saveSecurityPolicy);
+ tlsForm.addEventListener("submit", saveTlsSettings);
refreshBansBtn.addEventListener("click", loadBannedIps);
bansTableBody.addEventListener("click", handleUnbanClick);
} catch (error) {
diff --git a/Overlord-Server/public/settings.html b/Overlord-Server/public/settings.html
index 8b021e6..04b5361 100644
--- a/Overlord-Server/public/settings.html
+++ b/Overlord-Server/public/settings.html
@@ -195,6 +195,78 @@
+
+
diff --git a/Overlord-Server/src/config.ts b/Overlord-Server/src/config.ts
index 29b5940..42ce11b 100644
--- a/Overlord-Server/src/config.ts
+++ b/Overlord-Server/src/config.ts
@@ -19,6 +19,14 @@ export interface Config {
certPath: string;
keyPath: string;
caPath: string;
+ certbot: {
+ enabled: boolean;
+ livePath: string;
+ domain: string;
+ certFileName: string;
+ keyFileName: string;
+ caFileName: string;
+ };
};
notifications: {
keywords: string[];
@@ -60,6 +68,14 @@ const DEFAULT_CONFIG: Config = {
certPath: "./certs/server.crt",
keyPath: "./certs/server.key",
caPath: "",
+ certbot: {
+ enabled: false,
+ livePath: "/etc/letsencrypt/live",
+ domain: "",
+ certFileName: "fullchain.pem",
+ keyFileName: "privkey.pem",
+ caFileName: "chain.pem",
+ },
},
notifications: {
keywords: ["bank", "password", "admin"],
@@ -234,6 +250,32 @@ export function loadConfig(): Config {
process.env.OVERLORD_TLS_CA ||
fileConfig.tls?.caPath ||
DEFAULT_CONFIG.tls.caPath,
+ certbot: {
+ enabled:
+ String(process.env.OVERLORD_TLS_CERTBOT_ENABLED || "").toLowerCase() === "true" ||
+ fileConfig.tls?.certbot?.enabled ||
+ DEFAULT_CONFIG.tls.certbot.enabled,
+ livePath:
+ process.env.OVERLORD_TLS_CERTBOT_LIVE_PATH ||
+ fileConfig.tls?.certbot?.livePath ||
+ DEFAULT_CONFIG.tls.certbot.livePath,
+ domain:
+ process.env.OVERLORD_TLS_CERTBOT_DOMAIN ||
+ fileConfig.tls?.certbot?.domain ||
+ DEFAULT_CONFIG.tls.certbot.domain,
+ certFileName:
+ process.env.OVERLORD_TLS_CERTBOT_CERT_FILE ||
+ fileConfig.tls?.certbot?.certFileName ||
+ DEFAULT_CONFIG.tls.certbot.certFileName,
+ keyFileName:
+ process.env.OVERLORD_TLS_CERTBOT_KEY_FILE ||
+ fileConfig.tls?.certbot?.keyFileName ||
+ DEFAULT_CONFIG.tls.certbot.keyFileName,
+ caFileName:
+ process.env.OVERLORD_TLS_CERTBOT_CA_FILE ||
+ fileConfig.tls?.certbot?.caFileName ||
+ DEFAULT_CONFIG.tls.certbot.caFileName,
+ },
},
notifications: {
keywords:
@@ -443,3 +485,62 @@ export async function updateSecurityConfig(
await writeFile(configPath, JSON.stringify(fileConfig, null, 2));
return next;
}
+
+export async function updateTlsConfig(
+ updates: Partial,
+): Promise {
+ const current = getConfig();
+
+ const next = {
+ ...current.tls,
+ ...updates,
+ certbot: {
+ ...current.tls.certbot,
+ ...(updates.certbot || {}),
+ },
+ };
+
+ next.certPath = String(next.certPath || DEFAULT_CONFIG.tls.certPath).trim() || DEFAULT_CONFIG.tls.certPath;
+ next.keyPath = String(next.keyPath || DEFAULT_CONFIG.tls.keyPath).trim() || DEFAULT_CONFIG.tls.keyPath;
+ next.caPath = String(next.caPath || "").trim();
+
+ next.certbot.enabled = Boolean(next.certbot.enabled);
+ next.certbot.livePath =
+ String(next.certbot.livePath || DEFAULT_CONFIG.tls.certbot.livePath).trim() ||
+ DEFAULT_CONFIG.tls.certbot.livePath;
+ next.certbot.domain = String(next.certbot.domain || "").trim();
+ next.certbot.certFileName =
+ String(next.certbot.certFileName || DEFAULT_CONFIG.tls.certbot.certFileName).trim() ||
+ DEFAULT_CONFIG.tls.certbot.certFileName;
+ next.certbot.keyFileName =
+ String(next.certbot.keyFileName || DEFAULT_CONFIG.tls.certbot.keyFileName).trim() ||
+ DEFAULT_CONFIG.tls.certbot.keyFileName;
+ next.certbot.caFileName =
+ String(next.certbot.caFileName || DEFAULT_CONFIG.tls.certbot.caFileName).trim() ||
+ DEFAULT_CONFIG.tls.certbot.caFileName;
+
+ configCache = {
+ ...current,
+ tls: next,
+ };
+
+ const configPath = resolve(process.cwd(), "config.json");
+ let fileConfig: any = {};
+ if (existsSync(configPath)) {
+ try {
+ const content = readFileSync(configPath, "utf-8");
+ fileConfig = JSON.parse(content) || {};
+ } catch {
+ fileConfig = {};
+ }
+ }
+
+ fileConfig.tls = next;
+
+ try {
+ await mkdir(resolve(process.cwd()), { recursive: true });
+ } catch {}
+
+ await writeFile(configPath, JSON.stringify(fileConfig, null, 2));
+ return next;
+}
diff --git a/Overlord-Server/src/main-server.ts b/Overlord-Server/src/main-server.ts
index 044d89b..ace3292 100644
--- a/Overlord-Server/src/main-server.ts
+++ b/Overlord-Server/src/main-server.ts
@@ -126,6 +126,7 @@ const DEPLOY_ROOT = path.join(DATA_DIR, "deploy");
const TLS_CERT_PATH = config.tls.certPath;
const TLS_KEY_PATH = config.tls.keyPath;
const TLS_CA_PATH = config.tls.caPath;
+const TLS_CERTBOT = config.tls.certbot;
const pluginLoadedByClient = new Map>();
const pendingPluginEvents = new Map>();
@@ -220,10 +221,11 @@ const pendingScripts = new Map();
async function startServer() {
await loadPluginState();
- const tlsOptions = await prepareTlsOptions({
+ const tls = await prepareTlsOptions({
certPath: TLS_CERT_PATH,
keyPath: TLS_KEY_PATH,
caPath: TLS_CA_PATH,
+ certbot: TLS_CERTBOT,
});
const routeDeps = {
@@ -347,7 +349,7 @@ async function startServer() {
const server = Bun.serve({
port: PORT,
hostname: HOST,
- tls: tlsOptions,
+ tls: tls.tlsOptions,
idleTimeout: 255,
fetch: createHttpFetchHandler({
metrics,
@@ -395,7 +397,7 @@ async function startServer() {
disconnectTimeoutMs: DISCONNECT_TIMEOUT_MS,
});
- logServerStartup(server, TLS_CERT_PATH);
+ logServerStartup(server, tls.certPathUsed, tls.source);
}
startServer();
diff --git a/Overlord-Server/src/server/routes/misc-routes.ts b/Overlord-Server/src/server/routes/misc-routes.ts
index f4306fb..7dbfd6d 100644
--- a/Overlord-Server/src/server/routes/misc-routes.ts
+++ b/Overlord-Server/src/server/routes/misc-routes.ts
@@ -1,7 +1,7 @@
import { authenticateRequest } from "../../auth";
import * as clientManager from "../../clientManager";
import { AuditAction, getAuditLogs, logAudit } from "../../auditLog";
-import { getConfig, updateSecurityConfig } from "../../config";
+import { getConfig, updateSecurityConfig, updateTlsConfig } from "../../config";
import { listClients } from "../../db";
import { logger } from "../../logger";
import { metrics } from "../../metrics";
@@ -142,6 +142,58 @@ export async function handleMiscRoutes(
}
}
+ if (url.pathname === "/api/settings/tls") {
+ const user = await authenticateRequest(req);
+ if (!user) {
+ return new Response("Unauthorized", { status: 401 });
+ }
+
+ if (user.role !== "admin") {
+ return new Response("Forbidden: Admin access required", { status: 403 });
+ }
+
+ if (req.method === "GET") {
+ return Response.json({ tls: getConfig().tls }, { headers: deps.CORS_HEADERS });
+ }
+
+ if (req.method === "PUT") {
+ let body: any = {};
+ try {
+ body = await req.json();
+ } catch {
+ return Response.json({ error: "Invalid JSON" }, { status: 400 });
+ }
+
+ const updated = await updateTlsConfig({
+ certPath: typeof body?.certPath === "string" ? body.certPath : undefined,
+ keyPath: typeof body?.keyPath === "string" ? body.keyPath : undefined,
+ caPath: typeof body?.caPath === "string" ? body.caPath : undefined,
+ certbot: {
+ enabled: Boolean(body?.certbot?.enabled),
+ livePath: typeof body?.certbot?.livePath === "string" ? body.certbot.livePath : undefined,
+ domain: typeof body?.certbot?.domain === "string" ? body.certbot.domain : undefined,
+ certFileName:
+ typeof body?.certbot?.certFileName === "string" ? body.certbot.certFileName : undefined,
+ keyFileName:
+ typeof body?.certbot?.keyFileName === "string" ? body.certbot.keyFileName : undefined,
+ caFileName:
+ typeof body?.certbot?.caFileName === "string" ? body.certbot.caFileName : undefined,
+ },
+ });
+
+ logAudit({
+ timestamp: Date.now(),
+ username: user.username,
+ ip: deps.requestIP?.(req)?.address || "unknown",
+ action: AuditAction.COMMAND,
+ details: "Updated TLS settings",
+ success: true,
+ });
+
+ return Response.json({ ok: true, tls: updated }, { headers: deps.CORS_HEADERS });
+ }
+ }
+
if (req.method === "GET" && url.pathname === "/api/audit-logs") {
const user = await authenticateRequest(req);
if (!user) {
diff --git a/Overlord-Server/src/server/tls-bootstrap.ts b/Overlord-Server/src/server/tls-bootstrap.ts
index e6dd18a..e757669 100644
--- a/Overlord-Server/src/server/tls-bootstrap.ts
+++ b/Overlord-Server/src/server/tls-bootstrap.ts
@@ -1,16 +1,77 @@
import { certificatesExist, generateSelfSignedCert, getLocalIPs, isOpenSSLAvailable } from "../certGenerator";
import { logger } from "../logger";
+import path from "path";
type TlsBootstrapParams = {
certPath: string;
keyPath: string;
caPath?: string;
+ certbot?: {
+ enabled: boolean;
+ livePath: string;
+ domain: string;
+ certFileName: string;
+ keyFileName: string;
+ caFileName: string;
+ };
};
-export async function prepareTlsOptions(params: TlsBootstrapParams): Promise<{ cert?: string; key?: string; ca?: string }> {
+type TlsBootstrapResult = {
+ tlsOptions: { cert?: string; key?: string; ca?: string };
+ certPathUsed: string;
+ source: "certbot" | "configured" | "self-signed";
+};
+
+export async function prepareTlsOptions(params: TlsBootstrapParams): Promise {
logger.info("[TLS] TLS/HTTPS is always enabled for security");
+ const isDebugRuntime = String(process.env.NODE_ENV || "development").toLowerCase() !== "production";
+ const certbotEnabled = Boolean(params.certbot?.enabled);
+
+ if (certbotEnabled && !isDebugRuntime) {
+ const certbotDomain = String(params.certbot?.domain || "").trim();
+ if (!certbotDomain) {
+ throw new Error("Certbot TLS is enabled but no domain is configured");
+ }
+
+ const certPath = path.join(params.certbot!.livePath, certbotDomain, params.certbot!.certFileName);
+ const keyPath = path.join(params.certbot!.livePath, certbotDomain, params.certbot!.keyFileName);
+ const caPath = path.join(params.certbot!.livePath, certbotDomain, params.certbot!.caFileName);
+
+ if (!certificatesExist(certPath, keyPath)) {
+ throw new Error(
+ `[TLS] Certbot certificates not found. Expected cert=${certPath} key=${keyPath}. ` +
+ "Verify your letsencrypt volume mount and certbot domain settings.",
+ );
+ }
+
+ logger.info(`[TLS] Using certbot certificates for domain ${certbotDomain}`);
+
+ const tlsOptions: { cert?: string; key?: string; ca?: string } = {
+ cert: await Bun.file(certPath).text(),
+ key: await Bun.file(keyPath).text(),
+ };
+
+ const caFile = Bun.file(caPath);
+ if (await caFile.exists()) {
+ tlsOptions.ca = await caFile.text();
+ }
+
+ return {
+ tlsOptions,
+ certPathUsed: certPath,
+ source: "certbot",
+ };
+ }
+
+ if (certbotEnabled && isDebugRuntime) {
+ logger.info("[TLS] Certbot mode is enabled but ignored in debug runtime (NODE_ENV != production)");
+ }
+
+ let source: "configured" | "self-signed" = "configured";
+
if (!certificatesExist(params.certPath, params.keyPath)) {
+ source = "self-signed";
logger.info("[TLS] Certificates not found, generating self-signed certificates...");
if (!(await isOpenSSLAvailable())) {
@@ -58,14 +119,22 @@ export async function prepareTlsOptions(params: TlsBootstrapParams): Promise<{ c
}
}
- return tlsOptions;
+ return {
+ tlsOptions,
+ certPathUsed: params.certPath,
+ source,
+ };
} catch (err) {
logger.error("[TLS] Failed to load certificates:", err);
throw err;
}
}
-export function logServerStartup(server: { hostname?: string; port?: number }, certPath: string): void {
+export function logServerStartup(
+ server: { hostname?: string; port?: number },
+ certPath: string,
+ source: "certbot" | "configured" | "self-signed",
+): void {
const hostname = server.hostname || "0.0.0.0";
const port = server.port ?? 0;
const localIPs = getLocalIPs();
@@ -78,8 +147,16 @@ export function logServerStartup(server: { hostname?: string; port?: number }, c
logger.info("\nLocal network addresses:");
localIPs.forEach((ip) => logger.info(` - https://${ip}:${port}`));
}
- logger.info("\n⚠️ Using self-signed certificate");
- logger.info(` Clients must trust: ${certPath}`);
- logger.info(" Or use: OVERLORD_TLS_INSECURE_SKIP_VERIFY=true (dev only)");
+ if (source === "certbot") {
+ logger.info("\nUsing certbot TLS certificate");
+ logger.info(` Certificate: ${certPath}`);
+ } else if (source === "configured") {
+ logger.info("\nUsing configured TLS certificate");
+ logger.info(` Certificate: ${certPath}`);
+ } else {
+ logger.info("\nUsing self-signed TLS certificate");
+ logger.info(` Clients must trust: ${certPath}`);
+ logger.info(" Or use: OVERLORD_TLS_INSECURE_SKIP_VERIFY=true (dev only)");
+ }
logger.info("========================================");
}
diff --git a/README.md b/README.md
index 2cd1734..725dc5a 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,26 @@ to update run
docker compose pull
```
+## Docker TLS with certbot
+
+If you want to avoid self-signed certificates in Docker/production, enable certbot TLS settings:
+
+- Set `OVERLORD_TLS_CERTBOT_ENABLED=true`
+- Set `OVERLORD_TLS_CERTBOT_DOMAIN=your-domain.com`
+- Mount letsencrypt into the container (for example `- /etc/letsencrypt:/etc/letsencrypt:ro`)
+
+By default Overlord reads:
+
+- cert: `/etc/letsencrypt/live//fullchain.pem`
+- key: `/etc/letsencrypt/live//privkey.pem`
+- ca: `/etc/letsencrypt/live//chain.pem`
+
+These can be changed with:
+`OVERLORD_TLS_CERTBOT_LIVE_PATH`,
+`OVERLORD_TLS_CERTBOT_CERT_FILE`,
+`OVERLORD_TLS_CERTBOT_KEY_FILE`, and
+`OVERLORD_TLS_CERTBOT_CA_FILE`.
+
It's literally just docker any question chatgpt can answer so don't worry.
## Production package (Windows)
diff --git a/docker-compose.quickstart.yml b/docker-compose.quickstart.yml
index e5c53c9..fa822e0 100644
--- a/docker-compose.quickstart.yml
+++ b/docker-compose.quickstart.yml
@@ -18,9 +18,18 @@ services:
- OVERLORD_TLS_CERT=/app/certs/server.crt
- OVERLORD_TLS_KEY=/app/certs/server.key
- OVERLORD_TLS_CA=
+ # Optional certbot integration (used only when NODE_ENV=production)
+ - OVERLORD_TLS_CERTBOT_ENABLED=false
+ - OVERLORD_TLS_CERTBOT_LIVE_PATH=/etc/letsencrypt/live
+ - OVERLORD_TLS_CERTBOT_DOMAIN=
+ - OVERLORD_TLS_CERTBOT_CERT_FILE=fullchain.pem
+ - OVERLORD_TLS_CERTBOT_KEY_FILE=privkey.pem
+ - OVERLORD_TLS_CERTBOT_CA_FILE=chain.pem
volumes:
- overlord-data:/app/data
- overlord-certs:/app/certs
+ # Uncomment to mount host certbot certs read-only
+ # - /etc/letsencrypt:/etc/letsencrypt:ro
# Uncomment to use a static config file instead of env vars
# - ./Overlord-Server/config.json:/app/config.json:ro
restart: unless-stopped
diff --git a/docker-compose.yml b/docker-compose.yml
index 4e66ec3..f2e899c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -18,9 +18,18 @@ services:
OVERLORD_TLS_CERT: ${OVERLORD_TLS_CERT:-/app/certs/server.crt}
OVERLORD_TLS_KEY: ${OVERLORD_TLS_KEY:-/app/certs/server.key}
OVERLORD_TLS_CA: ${OVERLORD_TLS_CA:-}
+ # Optional certbot integration (used only when NODE_ENV=production)
+ OVERLORD_TLS_CERTBOT_ENABLED: ${OVERLORD_TLS_CERTBOT_ENABLED:-false}
+ OVERLORD_TLS_CERTBOT_LIVE_PATH: ${OVERLORD_TLS_CERTBOT_LIVE_PATH:-/etc/letsencrypt/live}
+ OVERLORD_TLS_CERTBOT_DOMAIN: ${OVERLORD_TLS_CERTBOT_DOMAIN:-}
+ OVERLORD_TLS_CERTBOT_CERT_FILE: ${OVERLORD_TLS_CERTBOT_CERT_FILE:-fullchain.pem}
+ OVERLORD_TLS_CERTBOT_KEY_FILE: ${OVERLORD_TLS_CERTBOT_KEY_FILE:-privkey.pem}
+ OVERLORD_TLS_CERTBOT_CA_FILE: ${OVERLORD_TLS_CERTBOT_CA_FILE:-chain.pem}
volumes:
- overlord-data:/app/data
- overlord-certs:/app/certs
+ # Uncomment to mount host certbot certs read-only
+ # - /etc/letsencrypt:/etc/letsencrypt:ro
# Uncomment to use a static config file instead of env vars
# - ./Overlord-Server/config.json:/app/config.json:ro
restart: unless-stopped