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 @@ +
+
+

TLS Certificates

+

Admin-only TLS source configuration. Changes apply after server restart.

+
+ + + + + +
+ + + + + +
+ + +
+ 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