2026-04-30 19:07:17 -06:00

5562 lines
189 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Complete reference for every .env variable in Changemaker Lite.">
<meta name="author" content="Bunker Operations">
<link rel="canonical" href="https://cmlite.org/docs/getting-started/environment-variables/">
<link rel="prev" href="../services/">
<link rel="next" href="../first-steps/">
<link rel="icon" href="../../../assets/favicon.svg">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.6">
<title>Environment Variables - Changemaker Lite</title>
<link rel="stylesheet" href="../../../assets/stylesheets/main.484c7ddc.min.css">
<link rel="stylesheet" href="../../../assets/stylesheets/palette.ab4e12ef.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:300,300i,400,400i,700,700i%7CJetBrains+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Inter";--md-code-font:"JetBrains Mono"}</style>
<link rel="stylesheet" href="../../../stylesheets/extra.css">
<link rel="stylesheet" href="../../../stylesheets/home.css">
<link rel="stylesheet" href="../../../stylesheets/docs-comments.css">
<link rel="stylesheet" href="../../../assets/css/video-player.css">
<link rel="stylesheet" href="../../../assets/css/image-gallery.css">
<link rel="stylesheet" href="../../../assets/css/payment-widgets.css">
<script>__md_scope=new URL("../../..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
<meta property="og:type" content="website" />
<meta property="og:title" content="Environment Variables - Changemaker Lite" />
<meta property="og:description" content="Complete reference for every .env variable in Changemaker Lite." />
<meta property="og:image" content="https://cmlite.org/assets/images/social/docs/getting-started/environment-variables.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://cmlite.org/docs/getting-started/environment-variables/" />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content="Environment Variables - Changemaker Lite" />
<meta property="twitter:description" content="Complete reference for every .env variable in Changemaker Lite." />
<meta property="twitter:image" content="https://cmlite.org/assets/images/social/docs/getting-started/environment-variables.png" />
</head>
<body dir="ltr" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="amber">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#environment-variables" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
<aside class="md-banner">
<div class="md-banner__inner md-grid md-typeset">
<button class="md-banner__button md-icon" aria-label="Don't show this again">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet">
<nav class="cm-header-nav" role="navigation" aria-label="Application">
<div class="cm-header-nav__brand">
<a href="#" data-path="/home" class="cm-header-nav__brand-link">
<span class="cm-header-nav__brand-text">Changemaker Lite</span>
</a>
</div>
<div class="cm-header-nav__links">
<div class="cm-header-nav__links-inner">
<a href="#" data-path="/" class="cm-header-nav__link" data-nav-id="home" target="_blank" rel="noopener noreferrer"><span class="material-icons-outlined">home</span><span class="cm-header-nav__label">Home</span></a>
<a href="#" data-path="/campaigns" class="cm-header-nav__link" data-nav-id="campaigns"><span class="material-icons-outlined">send</span><span class="cm-header-nav__label">Campaigns</span></a>
<a href="#" data-path="/map" class="cm-header-nav__link" data-nav-id="map"><span class="material-icons-outlined">place</span><span class="cm-header-nav__label">Map</span></a>
<a href="#" data-path="/shifts" class="cm-header-nav__link" data-nav-id="shifts"><span class="material-icons-outlined">event</span><span class="cm-header-nav__label">Shifts</span></a>
<a href="#" data-path="/events" class="cm-header-nav__link" data-nav-id="events" target="_blank" rel="noopener noreferrer"><span class="material-icons-outlined">event</span><span class="cm-header-nav__label">Events</span></a>
<a href="#" data-path="/gallery" class="cm-header-nav__link" data-nav-id="gallery"><span class="material-icons-outlined">play_circle</span><span class="cm-header-nav__label">Gallery</span></a>
<a href="#" data-path="/pricing" class="cm-header-nav__link" data-nav-id="pricing"><span class="material-icons-outlined">attach_money</span><span class="cm-header-nav__label">Pricing</span></a>
<a href="#" data-path="/shop" class="cm-header-nav__link" data-nav-id="shop"><span class="material-icons-outlined">shopping_bag</span><span class="cm-header-nav__label">Shop</span></a>
<a href="#" data-path="/donate" class="cm-header-nav__link" data-nav-id="donate"><span class="material-icons-outlined">favorite_border</span><span class="cm-header-nav__label">Donate</span></a>
<label for="__search" class="cm-header-nav__utility" title="Search">
<span class="material-icons-outlined">search</span>
</label>
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
<span class="material-icons-outlined">dark_mode</span>
</button>
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
<span class="material-icons-outlined">login</span>
<span class="cm-header-nav__label">Sign In</span>
</a>
<div class="cm-header-nav__dropdown" id="cm-admin-dropdown" style="display:none">
<span class="cm-header-nav__link cm-header-nav__dropdown-trigger">
<span class="material-icons-outlined">person</span>
<span class="cm-header-nav__label">Admin</span>
<span class="material-icons-outlined cm-header-nav__chevron">expand_more</span>
</span>
<div class="cm-header-nav__dropdown-menu cm-header-nav__dropdown-menu--right">
<a href="#" data-path="/app" class="cm-header-nav__dropdown-item"><span class="material-icons-outlined">dashboard</span><span>Admin Panel</span></a>
<a href="#" data-path="/volunteer" class="cm-header-nav__dropdown-item"><span class="material-icons-outlined">volunteer_activism</span><span>Volunteer Portal</span></a>
<a href="#" data-path="/volunteer/profile" class="cm-header-nav__dropdown-item"><span class="material-icons-outlined">account_circle</span><span>My Profile</span></a>
<a href="#" data-path="/logout" class="cm-header-nav__dropdown-item"><span class="material-icons-outlined">logout</span><span>Logout</span></a>
</div>
</div>
</div>
<button class="cm-header-nav__hamburger" aria-label="Open navigation menu">
<span class="material-icons-outlined">menu</span>
</button>
</div>
</nav>
<div class="cm-header-nav__mobile-drawer" id="cm-mobile-drawer">
<div class="cm-header-nav__mobile-header">
<span class="cm-header-nav__brand-text">Changemaker Lite</span>
<button class="cm-header-nav__mobile-close" aria-label="Close navigation menu">
<span class="material-icons-outlined">close</span>
</button>
</div>
<div class="cm-header-nav__mobile-links">
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
<span class="material-icons-outlined">search</span>
<span>Search</span>
</label>
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
<span class="material-icons-outlined">dark_mode</span>
<span>Dark Mode</span>
</button>
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
<span class="material-icons-outlined">menu_book</span>
<span>Docs Navigation</span>
</button>
<div class="cm-header-nav__mobile-divider"></div>
<a href="#" data-path="/" class="cm-header-nav__mobile-link" data-nav-id="home" target="_blank" rel="noopener noreferrer"><span class="material-icons-outlined">home</span><span>Home</span></a>
<a href="#" data-path="/campaigns" class="cm-header-nav__mobile-link" data-nav-id="campaigns"><span class="material-icons-outlined">send</span><span>Campaigns</span></a>
<a href="#" data-path="/map" class="cm-header-nav__mobile-link" data-nav-id="map"><span class="material-icons-outlined">place</span><span>Map</span></a>
<a href="#" data-path="/shifts" class="cm-header-nav__mobile-link" data-nav-id="shifts"><span class="material-icons-outlined">event</span><span>Shifts</span></a>
<a href="#" data-path="/events" class="cm-header-nav__mobile-link" data-nav-id="events" target="_blank" rel="noopener noreferrer"><span class="material-icons-outlined">event</span><span>Events</span></a>
<a href="#" data-path="/gallery" class="cm-header-nav__mobile-link" data-nav-id="gallery"><span class="material-icons-outlined">play_circle</span><span>Gallery</span></a>
<a href="#" data-path="/pricing" class="cm-header-nav__mobile-link" data-nav-id="pricing"><span class="material-icons-outlined">attach_money</span><span>Pricing</span></a>
<a href="#" data-path="/shop" class="cm-header-nav__mobile-link" data-nav-id="shop"><span class="material-icons-outlined">shopping_bag</span><span>Shop</span></a>
<a href="#" data-path="/donate" class="cm-header-nav__mobile-link" data-nav-id="donate"><span class="material-icons-outlined">favorite_border</span><span>Donate</span></a>
<div class="cm-header-nav__mobile-divider"></div>
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
<span class="material-icons-outlined">login</span>
<span>Sign In</span>
</a>
<div class="cm-header-nav__mobile-group" data-group-id="admin" id="cm-mobile-admin-group" style="display:none">
<span class="cm-header-nav__mobile-link cm-header-nav__mobile-group-trigger" role="button">
<span class="material-icons-outlined">person</span>
<span style="flex:1">Admin</span>
<span class="material-icons-outlined cm-header-nav__mobile-chevron">expand_more</span>
</span>
<div class="cm-header-nav__mobile-group-children">
<a href="#" data-path="/app" class="cm-header-nav__mobile-link" style="padding-left:48px"><span class="material-icons-outlined">dashboard</span><span>Admin Panel</span></a>
<a href="#" data-path="/volunteer" class="cm-header-nav__mobile-link" style="padding-left:48px"><span class="material-icons-outlined">volunteer_activism</span><span>Volunteer Portal</span></a>
<a href="#" data-path="/volunteer/profile" class="cm-header-nav__mobile-link" style="padding-left:48px"><span class="material-icons-outlined">account_circle</span><span>My Profile</span></a>
<a href="#" data-path="/logout" class="cm-header-nav__mobile-link" style="padding-left:48px"><span class="material-icons-outlined">logout</span><span>Logout</span></a>
</div>
</div>
</div>
</div>
<div class="cm-header-nav__mobile-overlay" id="cm-mobile-overlay"></div>
<script>
(function() {
var h = location.hostname;
var base;
if (h === 'localhost' || h === '127.0.0.1') {
base = location.protocol + '//localhost:' + (3002 || 3000);
} else {
var parts = h.split('.');
if (parts.length >= 3) { parts[0] = 'app'; }
else { parts.unshift('app'); }
base = location.protocol + '//' + parts.join('.');
}
var links = document.querySelectorAll('[data-path]');
for (var i = 0; i < links.length; i++) {
links[i].setAttribute('href', base + links[i].getAttribute('data-path'));
}
// Highlight active nav link based on current path
var path = location.pathname;
var activeLink = null;
if (path.indexOf('/docs') === 0) activeLink = 'docs';
document.querySelectorAll('.cm-header-nav__link[data-nav-id], .cm-header-nav__mobile-link[data-nav-id]').forEach(function(el) {
if (el.getAttribute('data-nav-id') === activeLink) {
el.classList.add('cm-header-nav__link--active');
}
});
// Hamburger toggle
var hamburger = document.querySelector('.cm-header-nav__hamburger');
var drawer = document.getElementById('cm-mobile-drawer');
var overlay = document.getElementById('cm-mobile-overlay');
var closeBtn = document.querySelector('.cm-header-nav__mobile-close');
function openDrawer() { drawer.classList.add('open'); overlay.classList.add('open'); }
function closeDrawer() { drawer.classList.remove('open'); overlay.classList.remove('open'); }
if (hamburger) hamburger.addEventListener('click', openDrawer);
if (closeBtn) closeBtn.addEventListener('click', closeDrawer);
if (overlay) overlay.addEventListener('click', closeDrawer);
// Mobile group expand/collapse toggles
document.querySelectorAll('.cm-header-nav__mobile-group-trigger').forEach(function(trigger) {
trigger.addEventListener('click', function() {
var group = this.closest('.cm-header-nav__mobile-group');
var children = group.querySelector('.cm-header-nav__mobile-group-children');
var isExpanded = group.classList.contains('expanded');
if (isExpanded) {
group.classList.remove('expanded');
children.style.display = 'none';
} else {
group.classList.add('expanded');
children.style.display = 'block';
}
});
});
// Auth-aware: show Admin dropdown for logged-in users, Sign In for guests.
// Uses hidden iframe + postMessage to read auth state from the app's origin.
function showAdminMenu() {
var s1 = document.getElementById('cm-signin-link');
var s2 = document.getElementById('cm-mobile-signin-link');
var a1 = document.getElementById('cm-admin-dropdown');
var a2 = document.getElementById('cm-mobile-admin-group');
if (s1) s1.style.display = 'none';
if (s2) s2.style.display = 'none';
if (a1) a1.style.display = '';
if (a2) a2.style.display = '';
}
// 1. Same-origin check (works when MkDocs served from same origin as app)
try {
var stored = localStorage.getItem('cml-auth');
if (stored) {
var parsed = JSON.parse(stored);
if (parsed && parsed.state && parsed.state.accessToken) {
showAdminMenu();
}
}
} catch(e) {}
// 2. Cross-origin check via hidden iframe + postMessage
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin);
window.addEventListener('message', function(event) {
if (event.origin !== base) return;
if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) {
showAdminMenu();
}
});
document.body.appendChild(iframe);
// Palette toggle (dark/light mode)
function togglePalette() {
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
for (var i = 0; i < inputs.length; i++) {
if (!inputs[i].checked) { inputs[i].click(); break; }
}
setTimeout(updatePaletteIcon, 50);
}
function updatePaletteIcon() {
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
var isDark = scheme === 'slate';
var icon = isDark ? 'light_mode' : 'dark_mode';
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
el.textContent = icon;
});
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
}
var ptBtn = document.getElementById('cm-palette-toggle');
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
// Docs sidebar toggle (opens Material's docs navigation drawer)
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
if (docsSidebarBtn) {
docsSidebarBtn.addEventListener('click', function() {
closeDrawer();
var dt = document.getElementById('__drawer');
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
});
}
// Close custom drawer when search label is clicked on mobile + auto-focus input
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
el.addEventListener('click', function() {
if (el.classList.contains('md-search__overlay')) return; // overlay has its own handler
closeDrawer();
setTimeout(function() {
var input = document.querySelector('.md-search__input');
if (input) input.focus();
}, 150);
});
});
// Search activation: Material may open search via checkbox OR by focusing the
// input directly (varies by version). Detect both and mirror as body class.
// NOTE: search DOM elements render AFTER the announce block in the template,
// so we must defer element queries until DOMContentLoaded.
var searchToggle = null;
var searchInput = null;
// Apply search layout inline styles (CSS-in-stylesheet is unreliable due to
// cross-origin Material stylesheets overriding !important rules)
function applySearchLayout(active) {
var inner = document.querySelector('.md-search__inner');
var output = document.querySelector('.md-search__output');
var scrollwrap = document.querySelector('.md-search__scrollwrap');
if (!inner) return;
var isDesktop = window.matchMedia('(min-width: 60em)').matches;
if (active) {
inner.style.setProperty('display', 'flex', 'important');
inner.style.setProperty('flex-direction', 'column', 'important');
inner.style.setProperty('overflow', 'hidden', 'important');
// Firefox needs explicit height (not just max-height) for flex children to grow
if (isDesktop) {
inner.style.setProperty('height', 'calc(100vh - 64px)', 'important');
}
if (output) {
output.style.setProperty('position', 'relative', 'important');
output.style.setProperty('flex', '1 1 0px', 'important');
output.style.setProperty('min-height', '0', 'important');
output.style.setProperty('display', 'flex', 'important');
output.style.setProperty('flex-direction', 'column', 'important');
output.style.setProperty('overflow', 'hidden', 'important');
output.style.setProperty('width', '100%', 'important');
}
if (scrollwrap) {
scrollwrap.style.setProperty('max-height', 'none', 'important');
scrollwrap.style.setProperty('flex', '1 1 0px', 'important');
scrollwrap.style.setProperty('min-height', '0', 'important');
scrollwrap.style.setProperty('overflow-y', 'auto', 'important');
}
// Force search result elements visible + ensure proper stacking (Firefox)
var resultList = document.querySelector('.md-search-result__list');
if (resultList) {
resultList.style.setProperty('display', 'block', 'important');
resultList.style.setProperty('visibility', 'visible', 'important');
resultList.style.setProperty('opacity', '1', 'important');
resultList.style.setProperty('max-height', 'none', 'important');
resultList.style.setProperty('overflow', 'visible', 'important');
resultList.style.setProperty('color', 'var(--md-default-fg-color)', 'important');
}
var resultContainer = document.querySelector('.md-search-result');
if (resultContainer) {
resultContainer.style.setProperty('display', 'block', 'important');
resultContainer.style.setProperty('visibility', 'visible', 'important');
resultContainer.style.setProperty('opacity', '1', 'important');
}
// Ensure scrollwrap has z-index above overlay
if (scrollwrap) {
scrollwrap.style.setProperty('position', 'relative', 'important');
scrollwrap.style.setProperty('z-index', '1', 'important');
scrollwrap.style.setProperty('background', 'var(--md-default-bg-color)', 'important');
}
} else {
inner.style.removeProperty('display');
inner.style.removeProperty('flex-direction');
inner.style.removeProperty('overflow');
inner.style.removeProperty('height');
if (output) {
output.style.removeProperty('position');
output.style.removeProperty('flex');
output.style.removeProperty('min-height');
output.style.removeProperty('display');
output.style.removeProperty('flex-direction');
output.style.removeProperty('overflow');
output.style.removeProperty('width');
}
if (scrollwrap) {
scrollwrap.style.removeProperty('max-height');
scrollwrap.style.removeProperty('flex');
scrollwrap.style.removeProperty('min-height');
scrollwrap.style.removeProperty('overflow-y');
scrollwrap.style.removeProperty('position');
scrollwrap.style.removeProperty('z-index');
scrollwrap.style.removeProperty('background');
}
var resultList = document.querySelector('.md-search-result__list');
if (resultList) resultList.removeAttribute('style');
var resultContainer = document.querySelector('.md-search-result');
if (resultContainer) resultContainer.removeAttribute('style');
}
}
function activateSearch() {
document.body.classList.add('cm-search-active');
if (searchToggle) searchToggle.checked = true;
applySearchLayout(true);
}
function deactivateSearch() {
document.body.classList.remove('cm-search-active');
if (searchToggle) searchToggle.checked = false;
if (searchInput) searchInput.blur();
applySearchLayout(false);
}
function isSearchActive() {
return document.body.classList.contains('cm-search-active');
}
// Custom search labels in the cm-header-nav (these exist now, in announce block)
document.querySelectorAll('label[for="__search"]').forEach(function(lbl) {
lbl.addEventListener('click', function() {
if (lbl.classList.contains('md-search__overlay')) return;
setTimeout(function() { activateSearch(); if (searchInput) searchInput.focus(); }, 50);
});
});
// Deferred bindings: attach handlers to search elements once they exist in the DOM
document.addEventListener('DOMContentLoaded', function() {
searchToggle = document.getElementById('__search');
searchInput = document.querySelector('.md-search__input');
// Detect search open via input focus
if (searchInput) {
searchInput.addEventListener('focus', activateSearch);
}
// Detect search open via checkbox
if (searchToggle) {
searchToggle.addEventListener('change', function() {
if (searchToggle.checked) activateSearch(); else deactivateSearch();
});
}
// Click on overlay (md-search__overlay label) to dismiss search
var searchOverlay = document.querySelector('.md-search__overlay');
if (searchOverlay) {
searchOverlay.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (isSearchActive()) deactivateSearch();
});
}
});
// Click-outside to dismiss search (on document, works immediately)
document.addEventListener('mousedown', function(e) {
if (!isSearchActive()) return;
var panel = document.querySelector('.md-search__inner');
if (panel && panel.contains(e.target)) return;
// Let the overlay's own click handler deal with it
if (e.target.closest && e.target.closest('.md-search__overlay')) return;
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
if (e.target.closest && e.target.closest('.cm-header-nav__utility')) return;
deactivateSearch();
});
// Escape key to dismiss
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && isSearchActive()) setTimeout(deactivateSearch, 50);
});
// Init palette icon + observe changes
setTimeout(updatePaletteIcon, 100);
new MutationObserver(function() { updatePaletteIcon(); })
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
})();
</script>
<style>
.md-banner {
background: transparent !important;
color: #ffffff !important;
padding: 0 !important;
margin: 0 !important;
overflow: visible !important;
border: none !important;
box-shadow: none !important;
position: relative;
z-index: 301;
}
.md-banner__inner {
overflow: visible !important;
margin: 0 !important;
padding: 0 !important;
max-width: 100% !important;
}
.md-banner__button {
display: none !important;
}
.cm-header-nav {
background: linear-gradient(135deg, #005a9c 0%, #007acc 100%);
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
position: relative;
z-index: 100;
box-sizing: border-box;
}
.cm-header-nav a {
color: rgba(255, 255, 255, 0.85) !important;
}
.cm-header-nav__brand-link {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none !important;
color: #fff !important;
}
.cm-header-nav__brand-text {
font-size: 18px;
font-weight: 600;
color: #fff !important;
}
.cm-header-nav__links {
display: flex;
align-items: center;
}
.cm-header-nav__links-inner {
display: flex;
align-items: center;
gap: 16px;
}
.cm-header-nav__link {
color: rgba(255, 255, 255, 0.85) !important;
text-decoration: none !important;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
transition: color 0.2s, border-color 0.2s;
white-space: nowrap;
padding-bottom: 2px;
border-bottom: 2px solid transparent;
}
.cm-header-nav__link:hover {
color: #fff !important;
text-decoration: none !important;
}
.cm-header-nav__link--active,
.cm-header-nav__link--active:hover {
color: #fff !important;
font-weight: 600;
border-bottom-color: #fff;
}
.cm-header-nav__link .material-icons-outlined {
font-size: 16px;
}
.cm-header-nav__hamburger {
display: none;
background: none;
border: none;
cursor: pointer;
padding: 4px 8px;
color: #fff;
}
.cm-header-nav__hamburger .material-icons-outlined {
font-size: 24px;
}
/* Desktop dropdown menus */
.cm-header-nav__dropdown {
position: relative;
display: inline-flex;
align-items: center;
}
.cm-header-nav__dropdown-trigger {
cursor: pointer;
user-select: none;
}
.cm-header-nav__dropdown-trigger .cm-header-nav__chevron {
font-size: 14px;
transition: transform 0.2s;
}
.cm-header-nav__dropdown:hover .cm-header-nav__chevron {
transform: rotate(180deg);
}
.cm-header-nav__dropdown-menu {
display: none;
position: absolute;
top: 100%;
left: 0;
min-width: 180px;
background: #1b2838;
border-radius: 8px;
padding: 6px 0;
box-shadow: 0 6px 16px rgba(0,0,0,0.3);
z-index: 100;
margin-top: 4px;
}
.cm-header-nav__dropdown:hover .cm-header-nav__dropdown-menu {
display: block;
}
.cm-header-nav__dropdown-menu--right {
left: auto;
right: 0;
}
.cm-header-nav__dropdown-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
color: rgba(255, 255, 255, 0.85) !important;
text-decoration: none !important;
font-size: 14px;
white-space: nowrap;
transition: background 0.15s;
}
.cm-header-nav__dropdown-item:hover {
background: rgba(255,255,255,0.1);
color: #fff !important;
text-decoration: none !important;
}
.cm-header-nav__dropdown-item .material-icons-outlined {
font-size: 16px;
}
/* Mobile drawer */
.cm-header-nav__mobile-drawer {
position: fixed;
top: 0;
right: -280px;
width: 280px;
height: 100vh;
background: #0d1b2a;
z-index: 10001;
transition: right 0.3s ease;
display: flex;
flex-direction: column;
}
.cm-header-nav__mobile-drawer.open {
right: 0;
}
.cm-header-nav__mobile-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-bottom: 1px solid rgba(255,255,255,0.1);
background: #1b2838;
}
.cm-header-nav__mobile-close {
background: none;
border: none;
cursor: pointer;
color: rgba(255,255,255,0.85);
padding: 4px;
}
.cm-header-nav__mobile-links {
display: flex;
flex-direction: column;
gap: 4px;
padding: 16px 0;
}
.cm-header-nav__mobile-link {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
color: rgba(255,255,255,0.85) !important;
text-decoration: none !important;
font-size: 15px;
border-radius: 4px;
}
.cm-header-nav__mobile-link:hover {
background: rgba(255,255,255,0.1);
color: #fff !important;
text-decoration: none !important;
}
.cm-header-nav__mobile-link--active {
color: #fff !important;
font-weight: 600;
background: rgba(255,255,255,0.1);
}
.cm-header-nav__mobile-link .material-icons-outlined {
font-size: 18px;
}
/* Mobile group expand/collapse */
.cm-header-nav__mobile-group-trigger {
cursor: pointer;
user-select: none;
}
.cm-header-nav__mobile-chevron {
font-size: 14px !important;
transition: transform 0.2s;
}
.cm-header-nav__mobile-group.expanded .cm-header-nav__mobile-chevron {
transform: rotate(180deg);
}
.cm-header-nav__mobile-group-children {
display: none;
}
.cm-header-nav__mobile-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 10000;
}
.cm-header-nav__mobile-overlay.open {
display: block;
}
@media (max-width: 768px) {
.cm-header-nav { padding: 0 16px; }
.cm-header-nav__links-inner { display: none; }
.cm-header-nav__hamburger { display: block; }
.cm-header-nav__dropdown-menu { display: none !important; }
}
/* Sidebar sticky offset = 0 since blue header scrolls away */
:root {
--md-header-height: 0px;
}
/* Hidden Material header — keeps search anchored near tabs */
.md-header--cm-hidden {
height: 0 !important;
min-height: 0 !important;
padding: 0 !important;
margin: 0 !important;
border: 0 !important;
overflow: visible !important;
background: transparent !important;
box-shadow: none !important;
position: sticky;
top: 0;
z-index: 200;
}
/* === DESKTOP SEARCH (>= 60em / 960px) === */
@media screen and (min-width: 60em) {
/* Fixed dropdown panel — layout (flex) applied via JS inline styles */
body.cm-search-active .md-header--cm-hidden .md-search__inner {
position: fixed !important;
top: 48px !important;
right: 16px !important;
left: auto !important;
width: min(34rem, calc(100vw - 32px)) !important;
max-height: calc(100vh - 64px) !important;
background: var(--md-default-bg-color) !important;
border-radius: 0 0 8px 8px !important;
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
z-index: 300 !important;
opacity: 1 !important;
transform: none !important;
visibility: visible !important;
pointer-events: auto !important;
clip-path: none !important;
}
/* Dark overlay behind search panel — catches clicks to dismiss */
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
background: rgba(0,0,0,0.54) !important;
opacity: 1 !important;
z-index: 299 !important;
border-radius: 0 !important;
transform: none !important;
cursor: default !important;
pointer-events: auto !important;
}
}
/* === MOBILE SEARCH (< 60em / 960px) === */
@media screen and (max-width: 59.984375em) {
/* Full-screen search takeover — layout (flex) applied via JS inline styles */
body.cm-search-active .md-header--cm-hidden .md-search__inner {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100% !important;
height: 100% !important;
opacity: 1 !important;
transform: none !important;
visibility: visible !important;
pointer-events: auto !important;
z-index: 300 !important;
background: var(--md-default-bg-color) !important;
clip-path: none !important;
}
}
/* Force search elements visible when active (layout handled by JS inline styles) */
body.cm-search-active .md-header--cm-hidden .md-search {
display: block !important;
visibility: visible !important;
opacity: 1 !important;
overflow: visible !important;
}
body.cm-search-active .md-header--cm-hidden .md-search__output {
opacity: 1 !important;
visibility: visible !important;
clip-path: none !important;
transform: none !important;
}
.cm-palette-container {
height: 0 !important;
overflow: hidden !important;
}
/* Material tabs: sticky at viewport top when blue header scrolls away */
.md-tabs {
position: sticky;
top: 0;
z-index: 99;
}
/* On mobile, hide tabs (sidebar provides navigation) */
@media (max-width: 768px) {
.md-tabs { display: none; }
}
/* Utility icon styling */
.cm-header-nav__utility {
background: none;
border: none;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
padding: 4px;
display: inline-flex;
align-items: center;
transition: color 0.2s;
}
.cm-header-nav__utility:hover { color: #fff; }
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
.cm-header-nav__utility-btn {
background: none;
border: none;
color: rgba(255,255,255,0.85);
cursor: pointer;
font-size: 15px;
font-family: inherit;
width: 100%;
text-align: left;
}
.cm-header-nav__mobile-divider {
height: 1px;
background: rgba(255,255,255,0.1);
margin: 8px 24px;
}
</style>
</div>
<script>var el=document.querySelector("[data-md-component=announce]");if(el){var content=el.querySelector(".md-typeset");__md_hash(content.innerHTML)===__md_get("__announce")&&(el.hidden=!0)}</script>
</aside>
</div>
<header class="md-header md-header--cm-hidden" data-md-component="header">
<div class="cm-palette-container">
<form class="md-header__option" data-md-component="palette">
<input class="md-option" data-md-color-media="" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="amber" aria-label="Switch to light mode" type="radio" name="__palette" id="__palette_0">
<label class="md-header__button md-icon" title="Switch to light mode" for="__palette_1" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m17.75 4.09-2.53 1.94.91 3.06-2.63-1.81-2.63 1.81.91-3.06-2.53-1.94L12.44 4l1.06-3 1.06 3zm3.5 6.91-1.64 1.25.59 1.98-1.7-1.17-1.7 1.17.59-1.98L15.75 11l2.06-.05L18.5 9l.69 1.95zm-2.28 4.95c.83-.08 1.72 1.1 1.19 1.85-.32.45-.66.87-1.08 1.27C15.17 23 8.84 23 4.94 19.07c-3.91-3.9-3.91-10.24 0-14.14.4-.4.82-.76 1.27-1.08.75-.53 1.93.36 1.85 1.19-.27 2.86.69 5.83 2.89 8.02a9.96 9.96 0 0 0 8.02 2.89m-1.64 2.02a12.08 12.08 0 0 1-7.8-3.47c-2.17-2.19-3.33-5-3.49-7.82-2.81 3.14-2.7 7.96.31 10.98 3.02 3.01 7.84 3.12 10.98.31"/></svg>
</label>
<input class="md-option" data-md-color-media="" data-md-color-scheme="default" data-md-color-primary="deep-purple" data-md-color-accent="amber" aria-label="Switch to dark mode" type="radio" name="__palette" id="__palette_1">
<label class="md-header__button md-icon" title="Switch to dark mode" for="__palette_0" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 7a5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5m0 2a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3m0-7 2.39 3.42C13.65 5.15 12.84 5 12 5s-1.65.15-2.39.42zM3.34 7l4.16-.35A7.2 7.2 0 0 0 5.94 8.5c-.44.74-.69 1.5-.83 2.29zm.02 10 1.76-3.77a7.131 7.131 0 0 0 2.38 4.14zM20.65 7l-1.77 3.79a7.02 7.02 0 0 0-2.38-4.15zm-.01 10-4.14.36c.59-.51 1.12-1.14 1.54-1.86.42-.73.69-1.5.83-2.29zM12 22l-2.41-3.44c.74.27 1.55.44 2.41.44.82 0 1.63-.17 2.37-.44z"/></svg>
</label>
</form>
</div>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<a href="javascript:void(0)" class="md-search__icon md-icon" title="Share" aria-label="Share" data-clipboard data-clipboard-text="" data-md-component="search-share" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3 3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66 0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08"/></svg>
</a>
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
<div class="md-search__suggest" data-md-component="search-suggest"></div>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
</header>
<div class="md-container" data-md-component="container">
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
<div class="md-grid">
<ul class="md-tabs__list">
<li class="md-tabs__item">
<a href="../../.." class="md-tabs__link">
Home
</a>
</li>
<li class="md-tabs__item md-tabs__item--active">
<a href="../../" class="md-tabs__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
Docs
</a>
</li>
<li class="md-tabs__item">
<a href="../../../blog/" class="md-tabs__link">
Blog
</a>
</li>
</ul>
</div>
</nav>
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary md-nav--lifted" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href="../../.." title="Changemaker Lite" class="md-nav__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
<img src="../../../assets/logo.svg" alt="logo">
</a>
Changemaker Lite
</label>
<div class="md-nav__source">
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
changemaker.lite
</div>
</a>
</div>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../.." class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2" checked>
<div class="md-nav__link md-nav__container">
<a href="../../" class="md-nav__link ">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
<span class="md-ellipsis">
Docs
</span>
</a>
<label class="md-nav__link " for="__nav_2" id="__nav_2_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_2_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_2">
<span class="md-nav__icon md-icon"></span>
Docs
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--active md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2_2" checked>
<div class="md-nav__link md-nav__container">
<a href="../" class="md-nav__link ">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m13.13 22.19-1.63-3.83c1.57-.58 3.04-1.36 4.4-2.27zM5.64 12.5l-3.83-1.63 6.1-2.77C7 9.46 6.22 10.93 5.64 12.5M21.61 2.39S16.66.269 11 5.93c-2.19 2.19-3.5 4.6-4.35 6.71-.28.75-.09 1.57.46 2.13l2.13 2.12c.55.56 1.37.74 2.12.46A19.1 19.1 0 0 0 18.07 13c5.66-5.66 3.54-10.61 3.54-10.61m-7.07 7.07c-.78-.78-.78-2.05 0-2.83s2.05-.78 2.83 0c.77.78.78 2.05 0 2.83s-2.05.78-2.83 0m-5.66 7.07-1.41-1.41zM6.24 22l3.64-3.64c-.34-.09-.67-.24-.97-.45L4.83 22zM2 22h1.41l4.77-4.76-1.42-1.41L2 20.59zm0-2.83 4.09-4.08c-.21-.3-.36-.62-.45-.97L2 17.76z"/></svg>
<span class="md-ellipsis">
Getting Started
</span>
</a>
<label class="md-nav__link " for="__nav_2_2" id="__nav_2_2_label" tabindex="0">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_2_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_2_2">
<span class="md-nav__icon md-icon"></span>
Getting Started
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../prerequisites/" class="md-nav__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 3h-4.18C14.4 1.84 13.3 1 12 1s-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2m-7 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1M7 7h10V5h2v14H5V5h2zm.5 6.5L9 12l2 2 4.5-4.5L17 11l-6 6z"/></svg>
<span class="md-ellipsis">
Prerequisites
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../installation/" class="md-nav__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 20h14v-2H5m14-9h-4V3H9v6H5l7 7z"/></svg>
<span class="md-ellipsis">
Installation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../services/" class="md-nav__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21.81 10.25c-.06-.04-.56-.43-1.64-.43-.28 0-.56.03-.84.08-.21-1.4-1.38-2.11-1.43-2.14l-.29-.17-.18.27c-.24.36-.43.77-.51 1.19-.2.8-.08 1.56.33 2.21-.49.28-1.29.35-1.46.35H2.62c-.34 0-.62.28-.62.63 0 1.15.18 2.3.58 3.38.45 1.19 1.13 2.07 2 2.61.98.6 2.59.94 4.42.94.79 0 1.61-.07 2.42-.22 1.12-.2 2.2-.59 3.19-1.16A8.3 8.3 0 0 0 16.78 16c1.05-1.17 1.67-2.5 2.12-3.65h.19c1.14 0 1.85-.46 2.24-.85.26-.24.45-.53.59-.87l.08-.24zm-17.96.99h1.76c.08 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16H3.85c-.09 0-.16.07-.16.16v1.58c.01.09.07.16.16.16m2.43 0h1.76c.08 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16H6.28c-.09 0-.16.07-.16.16v1.58c.01.09.07.16.16.16m2.47 0h1.75c.1 0 .17-.07.17-.16V9.5c0-.08-.06-.16-.17-.16H8.75c-.08 0-.15.07-.15.16v1.58c0 .09.06.16.15.16m2.44 0h1.77c.08 0 .15-.07.15-.16V9.5c0-.08-.06-.16-.15-.16h-1.77c-.08 0-.15.07-.15.16v1.58c0 .09.07.16.15.16M6.28 9h1.76c.08 0 .16-.09.16-.18V7.25c0-.09-.07-.16-.16-.16H6.28c-.09 0-.16.06-.16.16v1.57c.01.09.07.18.16.18m2.47 0h1.75c.1 0 .17-.09.17-.18V7.25c0-.09-.06-.16-.17-.16H8.75c-.08 0-.15.06-.15.16v1.57c0 .09.06.18.15.18m2.44 0h1.77c.08 0 .15-.09.15-.18V7.25c0-.09-.07-.16-.15-.16h-1.77c-.08 0-.15.06-.15.16v1.57c0 .09.07.18.15.18m0-2.28h1.77c.08 0 .15-.07.15-.16V5c0-.1-.07-.17-.15-.17h-1.77c-.08 0-.15.06-.15.17v1.56c0 .08.07.16.15.16m2.46 4.52h1.76c.09 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16h-1.76c-.08 0-.15.07-.15.16v1.58c0 .09.07.16.15.16"/></svg>
<span class="md-ellipsis">
Services Overview
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active">
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
<label class="md-nav__link md-nav__link--active" for="__toc">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 2c-1.11 0-2 .89-2 2v16a2 2 0 0 0 2 2h6.68a7 7 0 0 1-.68-3 7 7 0 0 1 7-7 7 7 0 0 1 1 .08V8l-6-6zm7 1.5L18.5 9H13zM18 14a.26.26 0 0 0-.26.21l-.19 1.32c-.3.13-.59.29-.85.47l-1.24-.5c-.11 0-.24 0-.31.13l-1 1.73c-.06.11-.04.24.06.32l1.06.82a4.2 4.2 0 0 0 0 1l-1.06.82a.26.26 0 0 0-.06.32l1 1.73c.06.13.19.13.31.13l1.24-.5c.26.18.54.35.85.47l.19 1.32c.02.12.12.21.26.21h2c.11 0 .22-.09.24-.21l.19-1.32c.3-.13.57-.29.84-.47l1.23.5c.13 0 .26 0 .33-.13l1-1.73a.26.26 0 0 0-.06-.32l-1.07-.82c.02-.17.04-.33.04-.5s-.01-.33-.04-.5l1.06-.82a.26.26 0 0 0 .06-.32l-1-1.73c-.06-.13-.19-.13-.32-.13l-1.23.5c-.27-.18-.54-.35-.85-.47l-.19-1.32A.236.236 0 0 0 20 14zm1 3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5c-.84 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5"/></svg>
<span class="md-ellipsis">
Environment Variables
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<a href="./" class="md-nav__link md-nav__link--active">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 2c-1.11 0-2 .89-2 2v16a2 2 0 0 0 2 2h6.68a7 7 0 0 1-.68-3 7 7 0 0 1 7-7 7 7 0 0 1 1 .08V8l-6-6zm7 1.5L18.5 9H13zM18 14a.26.26 0 0 0-.26.21l-.19 1.32c-.3.13-.59.29-.85.47l-1.24-.5c-.11 0-.24 0-.31.13l-1 1.73c-.06.11-.04.24.06.32l1.06.82a4.2 4.2 0 0 0 0 1l-1.06.82a.26.26 0 0 0-.06.32l1 1.73c.06.13.19.13.31.13l1.24-.5c.26.18.54.35.85.47l.19 1.32c.02.12.12.21.26.21h2c.11 0 .22-.09.24-.21l.19-1.32c.3-.13.57-.29.84-.47l1.23.5c.13 0 .26 0 .33-.13l1-1.73a.26.26 0 0 0-.06-.32l-1.07-.82c.02-.17.04-.33.04-.5s-.01-.33-.04-.5l1.06-.82a.26.26 0 0 0 .06-.32l-1-1.73c-.06-.13-.19-.13-.32-.13l-1.23.5c-.27-.18-.54-.35-.85-.47l-.19-1.32A.236.236 0 0 0 20 14zm1 3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5c-.84 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5"/></svg>
<span class="md-ellipsis">
Environment Variables
</span>
</a>
<nav class="md-nav md-nav--secondary" aria-label="On this page">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
On this page
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#quick-reference" class="md-nav__link">
<span class="md-ellipsis">
Quick Reference
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#general" class="md-nav__link">
<span class="md-ellipsis">
General
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#postgresql-main-database" class="md-nav__link">
<span class="md-ellipsis">
PostgreSQL (Main Database)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#jwt-authentication" class="md-nav__link">
<span class="md-ellipsis">
JWT Authentication
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#encryption-key" class="md-nav__link">
<span class="md-ellipsis">
Encryption Key
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#security-extras" class="md-nav__link">
<span class="md-ellipsis">
Security Extras
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#initial-admin-account" class="md-nav__link">
<span class="md-ellipsis">
Initial Admin Account
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#api-server" class="md-nav__link">
<span class="md-ellipsis">
API Server
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#admin-gui" class="md-nav__link">
<span class="md-ellipsis">
Admin GUI
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#rate-limiting" class="md-nav__link">
<span class="md-ellipsis">
Rate Limiting
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#nginx-reverse-proxy" class="md-nav__link">
<span class="md-ellipsis">
Nginx Reverse Proxy
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#redis" class="md-nav__link">
<span class="md-ellipsis">
Redis
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#payments-stripe" class="md-nav__link">
<span class="md-ellipsis">
Payments (Stripe)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#email-smtp" class="md-nav__link">
<span class="md-ellipsis">
Email / SMTP
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#listmonk-newsletters" class="md-nav__link">
<span class="md-ellipsis">
Listmonk (Newsletters)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#represent-api-canadian-electoral-data" class="md-nav__link">
<span class="md-ellipsis">
Represent API (Canadian Electoral Data)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#nocodb-data-browser" class="md-nav__link">
<span class="md-ellipsis">
NocoDB (Data Browser)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#media-manager" class="md-nav__link">
<span class="md-ellipsis">
Media Manager
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#gitea-git-hosting" class="md-nav__link">
<span class="md-ellipsis">
Gitea (Git Hosting)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#n8n-workflow-automation" class="md-nav__link">
<span class="md-ellipsis">
n8n (Workflow Automation)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#mkdocs-documentation" class="md-nav__link">
<span class="md-ellipsis">
MkDocs (Documentation)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#code-server-web-ide" class="md-nav__link">
<span class="md-ellipsis">
Code Server (Web IDE)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#homepage-service-dashboard" class="md-nav__link">
<span class="md-ellipsis">
Homepage (Service Dashboard)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#mini-qr-qr-code-generator" class="md-nav__link">
<span class="md-ellipsis">
Mini QR (QR Code Generator)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#excalidraw-whiteboard" class="md-nav__link">
<span class="md-ellipsis">
Excalidraw (Whiteboard)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#vaultwarden-password-manager" class="md-nav__link">
<span class="md-ellipsis">
Vaultwarden (Password Manager)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#rocketchat-team-chat" class="md-nav__link">
<span class="md-ellipsis">
Rocket.Chat (Team Chat)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#gancio-event-management" class="md-nav__link">
<span class="md-ellipsis">
Gancio (Event Management)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#jitsi-meet-video-conferencing" class="md-nav__link">
<span class="md-ellipsis">
Jitsi Meet (Video Conferencing)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#sms-campaigns-termux-android-bridge" class="md-nav__link">
<span class="md-ellipsis">
SMS Campaigns (Termux Android Bridge)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#mailhog-development-email" class="md-nav__link">
<span class="md-ellipsis">
MailHog (Development Email)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#nar-national-address-register" class="md-nav__link">
<span class="md-ellipsis">
NAR (National Address Register)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#geocoding" class="md-nav__link">
<span class="md-ellipsis">
Geocoding
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#overpass-area-import" class="md-nav__link">
<span class="md-ellipsis">
Overpass / Area Import
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#pangolin-tunnel" class="md-nav__link">
<span class="md-ellipsis">
Pangolin Tunnel
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#monitoring" class="md-nav__link">
<span class="md-ellipsis">
Monitoring
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#bunker-ops-fleet-management" class="md-nav__link">
<span class="md-ellipsis">
Bunker Ops (Fleet Management)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#social-people-analytics" class="md-nav__link">
<span class="md-ellipsis">
Social, People &amp; Analytics
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#geoip-maxmind-geolite2" class="md-nav__link">
<span class="md-ellipsis">
GeoIP (MaxMind GeoLite2)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#control-panel-agent-ccp" class="md-nav__link">
<span class="md-ellipsis">
Control Panel Agent (CCP)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#container-registry" class="md-nav__link">
<span class="md-ellipsis">
Container Registry
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#docker-container-management" class="md-nav__link">
<span class="md-ellipsis">
Docker / Container Management
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#embed-proxy-ports" class="md-nav__link">
<span class="md-ellipsis">
Embed Proxy Ports
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#gitea-docs-version-history" class="md-nav__link">
<span class="md-ellipsis">
Gitea Docs Version History
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#prisma-cli-host-side" class="md-nav__link">
<span class="md-ellipsis">
Prisma CLI (Host-Side)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#generating-secrets" class="md-nav__link">
<span class="md-ellipsis">
Generating Secrets
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#minimal-vs-full-deployment" class="md-nav__link">
<span class="md-ellipsis">
Minimal vs Full Deployment
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="../first-steps/" class="md-nav__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10.74 11.72c.47 1.23.42 2.51-.99 3.02-2.9 1.07-3.55-1.74-3.59-1.88zm-5.03-.81 4.32-1.07c-.19-1.05.1-2.1.1-3.34 0-1.68-1.33-4.97-3.45-4.44-2.42.6-2.77 3.29-2.68 4.59.12 1.3 1.64 4.08 1.71 4.26m12.14 8.94c-.03.15-.69 2.95-3.59 1.89-1.4-.52-1.46-1.8-.99-3.03zm2.15-6.2c.1-1.3-.24-4-2.67-4.6-2.11-.55-3.44 2.76-3.44 4.45 0 1.23.28 2.28.11 3.33l4.3 1.07c.08-.18 1.59-2.96 1.7-4.25"/></svg>
<span class="md-ellipsis">
First Steps
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../upgrades/" class="md-nav__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21 10.12h-6.78l2.74-2.82c-2.73-2.7-7.15-2.8-9.88-.1a6.887 6.887 0 0 0 0 9.8c2.73 2.7 7.15 2.7 9.88 0 1.36-1.35 2.04-2.92 2.04-4.9h2c0 1.98-.88 4.55-2.64 6.29-3.51 3.48-9.21 3.48-12.72 0-3.5-3.47-3.53-9.11-.02-12.58a8.987 8.987 0 0 1 12.65 0L21 3zM12.5 8v4.25l3.5 2.08-.72 1.21L11 13V8z"/></svg>
<span class="md-ellipsis">
Updates & Upgrades
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../control-panel/" class="md-nav__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 19V7H4v12zm0-16a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zm-7 14v-2h5v2zm-3.42-4L5.57 9H8.4l3.3 3.3c.39.39.39 1.03 0 1.42L8.42 17H5.59z"/></svg>
<span class="md-ellipsis">
Control Panel (CCP)
</span>
<span class="md-status md-status--new" title="Recently added">
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../features/" class="md-nav__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m18.09 11.77 1.47 6.33L14 14.74 8.44 18.1l1.46-6.33L5 7.5l6.47-.54L14 1l2.53 5.96L23 7.5zM2 12.43c.19 0 .38-.06.55-.17l3.2-2.11-1.57-1.36-2.73 1.8c-.461.3-.589.91-.29 1.41.2.27.52.43.84.43m-.84 9.12c.2.29.52.45.84.45.19 0 .38-.05.55-.16l4.11-2.71.34-1.37.31-1.45-5.86 3.85c-.461.31-.589.93-.29 1.39m.29-6.17a1 1 0 0 0-.29 1.38c.2.3.52.45.84.45.19 0 .38-.05.55-.16l5.42-3.55.27-1.19-.92-.81z"/></svg>
<span class="md-ellipsis">
Features at a Glance
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../admin/" class="md-nav__link">
<span class="md-ellipsis">
Admin Guide
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../user-guide/" class="md-nav__link">
<span class="md-ellipsis">
User Guide
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../volunteer/" class="md-nav__link">
<span class="md-ellipsis">
Volunteer Guide
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../deployment/" class="md-nav__link">
<span class="md-ellipsis">
Deployment
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../architecture/" class="md-nav__link">
<span class="md-ellipsis">
Architecture
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../services/" class="md-nav__link">
<span class="md-ellipsis">
Services
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../api/" class="md-nav__link">
<span class="md-ellipsis">
API Reference
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../troubleshooting/" class="md-nav__link">
<span class="md-ellipsis">
Troubleshooting
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item">
<a href="../../phil/" class="md-nav__link">
<span class="md-ellipsis">
Philosophy
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../blog/" class="md-nav__link">
<span class="md-ellipsis">
Blog
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--secondary" aria-label="On this page">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
On this page
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#quick-reference" class="md-nav__link">
<span class="md-ellipsis">
Quick Reference
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#general" class="md-nav__link">
<span class="md-ellipsis">
General
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#postgresql-main-database" class="md-nav__link">
<span class="md-ellipsis">
PostgreSQL (Main Database)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#jwt-authentication" class="md-nav__link">
<span class="md-ellipsis">
JWT Authentication
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#encryption-key" class="md-nav__link">
<span class="md-ellipsis">
Encryption Key
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#security-extras" class="md-nav__link">
<span class="md-ellipsis">
Security Extras
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#initial-admin-account" class="md-nav__link">
<span class="md-ellipsis">
Initial Admin Account
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#api-server" class="md-nav__link">
<span class="md-ellipsis">
API Server
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#admin-gui" class="md-nav__link">
<span class="md-ellipsis">
Admin GUI
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#rate-limiting" class="md-nav__link">
<span class="md-ellipsis">
Rate Limiting
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#nginx-reverse-proxy" class="md-nav__link">
<span class="md-ellipsis">
Nginx Reverse Proxy
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#redis" class="md-nav__link">
<span class="md-ellipsis">
Redis
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#payments-stripe" class="md-nav__link">
<span class="md-ellipsis">
Payments (Stripe)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#email-smtp" class="md-nav__link">
<span class="md-ellipsis">
Email / SMTP
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#listmonk-newsletters" class="md-nav__link">
<span class="md-ellipsis">
Listmonk (Newsletters)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#represent-api-canadian-electoral-data" class="md-nav__link">
<span class="md-ellipsis">
Represent API (Canadian Electoral Data)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#nocodb-data-browser" class="md-nav__link">
<span class="md-ellipsis">
NocoDB (Data Browser)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#media-manager" class="md-nav__link">
<span class="md-ellipsis">
Media Manager
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#gitea-git-hosting" class="md-nav__link">
<span class="md-ellipsis">
Gitea (Git Hosting)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#n8n-workflow-automation" class="md-nav__link">
<span class="md-ellipsis">
n8n (Workflow Automation)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#mkdocs-documentation" class="md-nav__link">
<span class="md-ellipsis">
MkDocs (Documentation)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#code-server-web-ide" class="md-nav__link">
<span class="md-ellipsis">
Code Server (Web IDE)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#homepage-service-dashboard" class="md-nav__link">
<span class="md-ellipsis">
Homepage (Service Dashboard)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#mini-qr-qr-code-generator" class="md-nav__link">
<span class="md-ellipsis">
Mini QR (QR Code Generator)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#excalidraw-whiteboard" class="md-nav__link">
<span class="md-ellipsis">
Excalidraw (Whiteboard)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#vaultwarden-password-manager" class="md-nav__link">
<span class="md-ellipsis">
Vaultwarden (Password Manager)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#rocketchat-team-chat" class="md-nav__link">
<span class="md-ellipsis">
Rocket.Chat (Team Chat)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#gancio-event-management" class="md-nav__link">
<span class="md-ellipsis">
Gancio (Event Management)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#jitsi-meet-video-conferencing" class="md-nav__link">
<span class="md-ellipsis">
Jitsi Meet (Video Conferencing)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#sms-campaigns-termux-android-bridge" class="md-nav__link">
<span class="md-ellipsis">
SMS Campaigns (Termux Android Bridge)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#mailhog-development-email" class="md-nav__link">
<span class="md-ellipsis">
MailHog (Development Email)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#nar-national-address-register" class="md-nav__link">
<span class="md-ellipsis">
NAR (National Address Register)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#geocoding" class="md-nav__link">
<span class="md-ellipsis">
Geocoding
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#overpass-area-import" class="md-nav__link">
<span class="md-ellipsis">
Overpass / Area Import
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#pangolin-tunnel" class="md-nav__link">
<span class="md-ellipsis">
Pangolin Tunnel
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#monitoring" class="md-nav__link">
<span class="md-ellipsis">
Monitoring
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#bunker-ops-fleet-management" class="md-nav__link">
<span class="md-ellipsis">
Bunker Ops (Fleet Management)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#social-people-analytics" class="md-nav__link">
<span class="md-ellipsis">
Social, People &amp; Analytics
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#geoip-maxmind-geolite2" class="md-nav__link">
<span class="md-ellipsis">
GeoIP (MaxMind GeoLite2)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#control-panel-agent-ccp" class="md-nav__link">
<span class="md-ellipsis">
Control Panel Agent (CCP)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#container-registry" class="md-nav__link">
<span class="md-ellipsis">
Container Registry
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#docker-container-management" class="md-nav__link">
<span class="md-ellipsis">
Docker / Container Management
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#embed-proxy-ports" class="md-nav__link">
<span class="md-ellipsis">
Embed Proxy Ports
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#gitea-docs-version-history" class="md-nav__link">
<span class="md-ellipsis">
Gitea Docs Version History
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#prisma-cli-host-side" class="md-nav__link">
<span class="md-ellipsis">
Prisma CLI (Host-Side)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#generating-secrets" class="md-nav__link">
<span class="md-ellipsis">
Generating Secrets
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#minimal-vs-full-deployment" class="md-nav__link">
<span class="md-ellipsis">
Minimal vs Full Deployment
</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<nav class="md-path" aria-label="Navigation" >
<ol class="md-path__list">
<li class="md-path__item">
<a href="../../.." class="md-path__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-path__item">
<a href="../../" class="md-path__link">
<span class="md-ellipsis">
Docs
</span>
</a>
</li>
<li class="md-path__item">
<a href="../" class="md-path__link">
<span class="md-ellipsis">
Getting Started
</span>
</a>
</li>
</ol>
</nav>
<article class="md-content__inner md-typeset">
<nav class="md-tags" >
<span class="md-tag">configuration</span>
<span class="md-tag">getting-started</span>
<span class="md-tag">operator</span>
<span class="md-tag">reference</span>
</nav>
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/getting-started/environment-variables.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/getting-started/environment-variables.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
<h1 id="environment-variables">Environment Variables<a class="headerlink" href="#environment-variables" title="Permanent link">&para;</a></h1>
<div class="admonition tip">
<p class="admonition-title">Need help getting set up?</p>
<p><strong>Bunker Operations</strong> provides managed infrastructure and hands-on setup assistance for organizations running Changemaker Lite. We handle domains, tunnels, SMTP, and servers so you can focus on your campaign. <strong>Get in touch:</strong> <a href="https://bnkops.com">bnkops.com</a> | <code>admin@bnkops.ca</code></p>
</div>
<p>Changemaker Lite uses a single <code>.env</code> file at the project root to configure all services. Copy the example file to get started:</p>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a>cp<span class="w"> </span>.env.example<span class="w"> </span>.env
</span></code></pre></div>
<div class="admonition danger">
<p class="admonition-title">Security Essentials</p>
<ul>
<li><strong>Change every</strong> <code>REQUIRED_STRONG_PASSWORD_CHANGE_THIS</code> value before starting services</li>
<li><strong>Generate secrets</strong> with <code>openssl rand -hex 32</code> (or <code>-hex 16</code> where noted)</li>
<li><strong>Never</strong> commit <code>.env</code> to version control</li>
<li>Use unique values for each secret &mdash; do not reuse JWT secrets as encryption keys</li>
</ul>
</div>
<hr />
<h2 id="quick-reference">Quick Reference<a class="headerlink" href="#quick-reference" title="Permanent link">&para;</a></h2>
<p>Variables are grouped by service. Each table marks whether a variable is <strong>required</strong> for a basic deployment or <strong>optional</strong> (has a sensible default or only needed for specific features).</p>
<table>
<thead>
<tr>
<th>Symbol</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span></td>
<td>Must be set before first run</td>
</tr>
<tr>
<td><span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span></td>
<td>Has a working default; change for production</td>
</tr>
<tr>
<td><span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span></td>
<td>Feature flag &mdash; opt-in</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="general">General<a class="headerlink" href="#general" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>NODE_ENV</code></td>
<td><code>development</code></td>
<td>Set to <code>production</code> for production deployments. Controls logging, error detail, and security checks.</td>
</tr>
<tr>
<td><code>DOMAIN</code></td>
<td><code>cmlite.org</code></td>
<td>Root domain. Used for nginx subdomain routing (<code>app.DOMAIN</code>, <code>api.DOMAIN</code>, etc.). The root domain serves the MkDocs documentation site; all application routes live under <code>app.DOMAIN</code>.</td>
</tr>
<tr>
<td><code>USER_ID</code></td>
<td><code>1000</code></td>
<td>UID for container file ownership. Match your host user's UID (<code>id -u</code>).</td>
</tr>
<tr>
<td><code>GROUP_ID</code></td>
<td><code>1000</code></td>
<td>GID for container file ownership. Match your host user's GID (<code>id -g</code>).</td>
</tr>
<tr>
<td><code>DOCKER_GROUP_ID</code></td>
<td><code>984</code></td>
<td>GID of the <code>docker</code> group on the host. Needed for containers that access the Docker socket. Find with <code>getent group docker</code>.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="postgresql-main-database">PostgreSQL (Main Database) <span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span><a class="headerlink" href="#postgresql-main-database" title="Permanent link">&para;</a></h2>
<p>The primary database for both the Express API and the Fastify Media API (shared).</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>V2_POSTGRES_USER</code></td>
<td><code>changemaker</code></td>
<td>Database username.</td>
</tr>
<tr>
<td><code>V2_POSTGRES_PASSWORD</code></td>
<td>&mdash;</td>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span> <strong>Must change.</strong> Database password.</td>
</tr>
<tr>
<td><code>V2_POSTGRES_DB</code></td>
<td><code>changemaker_v2</code></td>
<td>Database name.</td>
</tr>
<tr>
<td><code>V2_POSTGRES_PORT</code></td>
<td><code>5433</code></td>
<td>Host port mapping. The container listens on <code>5432</code> internally.</td>
</tr>
</tbody>
</table>
<div class="admonition tip">
<p class="admonition-title">Connection string</p>
<p>The <code>DATABASE_URL</code> is constructed automatically inside Docker. If running locally, set:
<div class="language-text highlight"><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1" href="#__codelineno-1-1"></a>DATABASE_URL=postgresql://changemaker:YOUR_PASSWORD@localhost:5433/changemaker_v2
</span></code></pre></div></p>
</div>
<hr />
<h2 id="jwt-authentication">JWT Authentication <span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span><a class="headerlink" href="#jwt-authentication" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>JWT_ACCESS_SECRET</code></td>
<td>&mdash;</td>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span> Secret for signing access tokens. Generate with <code>openssl rand -hex 32</code>.</td>
</tr>
<tr>
<td><code>JWT_REFRESH_SECRET</code></td>
<td>&mdash;</td>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span> Secret for signing refresh tokens. <strong>Must differ</strong> from the access secret.</td>
</tr>
<tr>
<td><code>JWT_INVITE_SECRET</code></td>
<td>&mdash;</td>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span> Secret for signing volunteer invite tokens. <strong>Must differ</strong> from access and refresh secrets. Generate with <code>openssl rand -hex 32</code>.</td>
</tr>
<tr>
<td><code>JWT_ACCESS_EXPIRY</code></td>
<td><code>15m</code></td>
<td>Access token lifetime. Short-lived by design.</td>
</tr>
<tr>
<td><code>JWT_REFRESH_EXPIRY</code></td>
<td><code>7d</code></td>
<td>Refresh token lifetime. Tokens are rotated atomically on each refresh.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="encryption-key">Encryption Key <span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span><a class="headerlink" href="#encryption-key" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ENCRYPTION_KEY</code></td>
<td>&mdash;</td>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span> AES key for encrypting secrets stored in the database (SMTP passwords, API keys, etc.). Generate with <code>openssl rand -hex 32</code>. <strong>Must not</strong> reuse a JWT secret. <strong>Required in all environments</strong> (no longer falls back to JWT secret in development).</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="security-extras">Security Extras <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span><a class="headerlink" href="#security-extras" title="Permanent link">&para;</a></h2>
<p>Additional secrets for key separation. These fall back to <code>JWT_ACCESS_SECRET</code> if empty, but setting unique values is <strong>strongly recommended</strong> for production.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>GITEA_SSO_SECRET</code></td>
<td><em>(empty)</em></td>
<td>Cookie signing secret for Gitea SSO integration. Falls back to <code>JWT_ACCESS_SECRET</code> if empty. Generate with <code>openssl rand -hex 32</code>.</td>
</tr>
<tr>
<td><code>SERVICE_PASSWORD_SALT</code></td>
<td><em>(empty)</em></td>
<td>Salt for deriving deterministic service passwords (Gitea, Rocket.Chat user provisioning). Falls back to <code>JWT_ACCESS_SECRET</code> if empty &mdash; rotating JWT_ACCESS_SECRET would then invalidate all provisioned service passwords. Generate with <code>openssl rand -hex 32</code>.</td>
</tr>
<tr>
<td><code>CSP_ENABLED</code></td>
<td><code>false</code></td>
<td>Enable Content Security Policy headers in API responses.</td>
</tr>
</tbody>
</table>
<div class="admonition warning">
<p class="admonition-title">Key separation</p>
<p>If <code>GITEA_SSO_SECRET</code> and <code>SERVICE_PASSWORD_SALT</code> are left empty, the API logs security warnings on every startup. Set unique values to isolate secret rotation and prevent one compromised key from affecting other subsystems.</p>
</div>
<hr />
<h2 id="initial-admin-account">Initial Admin Account <span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span><a class="headerlink" href="#initial-admin-account" title="Permanent link">&para;</a></h2>
<p>These credentials create the first super-admin user during database seeding (<code>npx prisma db seed</code>).</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>INITIAL_ADMIN_EMAIL</code></td>
<td><code>admin@cmlite.org</code></td>
<td>Email address for the initial admin.</td>
</tr>
<tr>
<td><code>INITIAL_ADMIN_PASSWORD</code></td>
<td>&mdash;</td>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span> <strong>Must change.</strong> Must be 12+ characters with uppercase, lowercase, and a digit. Change this password after first login.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="api-server">API Server<a class="headerlink" href="#api-server" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>API_PORT</code></td>
<td><code>4000</code></td>
<td>Host port for the Express API.</td>
</tr>
<tr>
<td><code>API_URL</code></td>
<td><code>http://localhost:4000</code></td>
<td>Public URL of the API. Used for generating links in emails and QR codes.</td>
</tr>
<tr>
<td><code>CORS_ORIGINS</code></td>
<td><code>http://localhost:3000,http://localhost</code></td>
<td>Comma-separated list of allowed CORS origins. <strong>Add your production domain</strong> (e.g., <code>https://app.yourdomain.org</code>) for production.</td>
</tr>
</tbody>
</table>
<div class="admonition warning">
<p class="admonition-title">Production CORS</p>
<p>If you deploy behind a tunnel (Pangolin, Cloudflare) and API requests fail with CORS errors, add your production <code>app.</code> subdomain here:
<div class="language-text highlight"><pre><span></span><code><span id="__span-2-1"><a id="__codelineno-2-1" name="__codelineno-2-1" href="#__codelineno-2-1"></a>CORS_ORIGINS=https://app.betteredmonton.org,http://localhost:3000,http://localhost
</span></code></pre></div>
Then restart the API: <code>docker compose restart api</code></p>
</div>
<hr />
<h2 id="admin-gui">Admin GUI<a class="headerlink" href="#admin-gui" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ADMIN_PORT</code></td>
<td><code>3000</code></td>
<td>Host port for the React admin dashboard.</td>
</tr>
<tr>
<td><code>ADMIN_URL</code></td>
<td><code>http://localhost:3000</code></td>
<td>Public URL of the admin GUI.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="rate-limiting">Rate Limiting <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span><a class="headerlink" href="#rate-limiting" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>RATE_LIMIT_WINDOW_MS</code></td>
<td><code>900000</code></td>
<td>Rate limit window in milliseconds (default: 15 minutes).</td>
</tr>
<tr>
<td><code>RATE_LIMIT_MAX</code></td>
<td><code>500</code></td>
<td>Maximum requests per window per IP. Auth endpoints have a stricter limit (10/min).</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="nginx-reverse-proxy">Nginx Reverse Proxy<a class="headerlink" href="#nginx-reverse-proxy" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>NGINX_HTTP_PORT</code></td>
<td><code>80</code></td>
<td>HTTP port. All subdomains route through nginx.</td>
</tr>
<tr>
<td><code>NGINX_HTTPS_PORT</code></td>
<td><code>443</code></td>
<td>HTTPS port. SSL is typically handled by the tunnel provider (Pangolin/Cloudflare).</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="redis">Redis <span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span><a class="headerlink" href="#redis" title="Permanent link">&para;</a></h2>
<p>Shared by rate limiting, BullMQ job queues, geocoding cache, and session data.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>REDIS_PASSWORD</code></td>
<td>&mdash;</td>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span> <strong>Must change.</strong> Redis requires authentication.</td>
</tr>
<tr>
<td><code>REDIS_URL</code></td>
<td><code>redis://:${REDIS_PASSWORD}@redis-changemaker:6379</code></td>
<td>Full connection URL. Uses the password variable automatically.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="payments-stripe">Payments (Stripe) <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span><a class="headerlink" href="#payments-stripe" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ENABLE_PAYMENTS</code></td>
<td><code>false</code></td>
<td><span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span> Set to <code>true</code> to enable the payments feature (memberships, products, donations). Stripe API keys are stored encrypted in the database via the admin settings page.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="email-smtp">Email / SMTP <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span><a class="headerlink" href="#email-smtp" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>SMTP_HOST</code></td>
<td><code>mailhog-changemaker</code></td>
<td>SMTP server. Default points to the MailHog dev container.</td>
</tr>
<tr>
<td><code>SMTP_PORT</code></td>
<td><code>1025</code></td>
<td>SMTP port. <code>1025</code> for MailHog, <code>587</code> for most production SMTP.</td>
</tr>
<tr>
<td><code>SMTP_USER</code></td>
<td><em>(empty)</em></td>
<td>SMTP username. Not needed for MailHog.</td>
</tr>
<tr>
<td><code>SMTP_PASS</code></td>
<td><em>(empty)</em></td>
<td>SMTP password.</td>
</tr>
<tr>
<td><code>SMTP_FROM</code></td>
<td><code>noreply@cmlite.org</code></td>
<td>"From" address on outgoing emails.</td>
</tr>
<tr>
<td><code>SMTP_FROM_NAME</code></td>
<td><code>Changemaker Lite</code></td>
<td>Display name for the "From" header.</td>
</tr>
<tr>
<td><code>EMAIL_TEST_MODE</code></td>
<td><code>true</code></td>
<td>When <code>true</code>, all emails go to MailHog instead of real SMTP. <strong>Set to <code>false</code> in production.</strong></td>
</tr>
<tr>
<td><code>TEST_EMAIL_RECIPIENT</code></td>
<td><code>admin@cmlite.org</code></td>
<td>Catch-all recipient when test mode is on.</td>
</tr>
</tbody>
</table>
<div class="admonition info">
<p class="admonition-title">Development email</p>
<p>With <code>EMAIL_TEST_MODE=true</code>, all outgoing email is captured in MailHog at <code>http://localhost:8025</code>. No real emails are sent.</p>
</div>
<hr />
<h2 id="listmonk-newsletters">Listmonk (Newsletters) <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span><a class="headerlink" href="#listmonk-newsletters" title="Permanent link">&para;</a></h2>
<p>Listmonk handles newsletter/marketing campaigns. Sync with the main platform is opt-in.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>LISTMONK_PORT</code></td>
<td><code>9001</code></td>
<td>Listmonk web UI port.</td>
</tr>
<tr>
<td><code>LISTMONK_DB_PORT</code></td>
<td><code>5434</code></td>
<td>Listmonk's own PostgreSQL port (separate from the main DB). Uses 5434 to avoid conflict with the main PostgreSQL (5432 internal / 5433 host).</td>
</tr>
<tr>
<td><code>LISTMONK_DB_USER</code></td>
<td><code>listmonk</code></td>
<td>Listmonk database user.</td>
</tr>
<tr>
<td><code>LISTMONK_DB_PASSWORD</code></td>
<td>&mdash;</td>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span> Listmonk database password.</td>
</tr>
<tr>
<td><code>LISTMONK_DB_NAME</code></td>
<td><code>listmonk</code></td>
<td>Listmonk database name.</td>
</tr>
<tr>
<td><code>LISTMONK_WEB_ADMIN_USER</code></td>
<td><code>admin</code></td>
<td>Login for the Listmonk web dashboard.</td>
</tr>
<tr>
<td><code>LISTMONK_WEB_ADMIN_PASSWORD</code></td>
<td>&mdash;</td>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span> Password for the Listmonk web dashboard.</td>
</tr>
<tr>
<td><code>LISTMONK_API_USER</code></td>
<td><code>v2-api</code></td>
<td>API user for programmatic access (auto-created by init container).</td>
</tr>
<tr>
<td><code>LISTMONK_API_TOKEN</code></td>
<td>&mdash;</td>
<td>Token for API user. Generate with <code>openssl rand -hex 16</code>.</td>
</tr>
<tr>
<td><code>LISTMONK_ADMIN_USER</code></td>
<td><code>v2-api</code></td>
<td>Same as <code>LISTMONK_API_USER</code> (used by the sync service).</td>
</tr>
<tr>
<td><code>LISTMONK_ADMIN_PASSWORD</code></td>
<td>&mdash;</td>
<td>Same as <code>LISTMONK_API_TOKEN</code>.</td>
</tr>
<tr>
<td><code>LISTMONK_SYNC_ENABLED</code></td>
<td><code>false</code></td>
<td><span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span> Set to <code>true</code> to sync participants/locations/users to Listmonk lists.</td>
</tr>
<tr>
<td><code>LISTMONK_WEBHOOK_SECRET</code></td>
<td><em>(empty)</em></td>
<td>Shared secret for Listmonk webhook callbacks.</td>
</tr>
<tr>
<td><code>LISTMONK_PROXY_PORT</code></td>
<td><code>9002</code></td>
<td>Nginx proxy port for Listmonk.</td>
</tr>
</tbody>
</table>
<details class="example">
<summary>Listmonk SMTP settings</summary>
<p>Listmonk has its own SMTP configuration, separate from the main platform's:</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>LISTMONK_SMTP_HOST</code></td>
<td><code>mailhog-changemaker</code></td>
<td>SMTP host for Listmonk.</td>
</tr>
<tr>
<td><code>LISTMONK_SMTP_PORT</code></td>
<td><code>1025</code></td>
<td>SMTP port.</td>
</tr>
<tr>
<td><code>LISTMONK_SMTP_USER</code></td>
<td><em>(empty)</em></td>
<td>SMTP username.</td>
</tr>
<tr>
<td><code>LISTMONK_SMTP_PASSWORD</code></td>
<td><em>(empty)</em></td>
<td>SMTP password.</td>
</tr>
<tr>
<td><code>LISTMONK_SMTP_TLS_TYPE</code></td>
<td><code>none</code></td>
<td>TLS mode: <code>none</code>, <code>STARTTLS</code>, or <code>TLS</code>.</td>
</tr>
<tr>
<td><code>LISTMONK_SMTP_FROM</code></td>
<td><code>Changemaker Lite &lt;noreply@cmlite.org&gt;</code></td>
<td>From address for newsletters.</td>
</tr>
</tbody>
</table>
</details>
<hr />
<h2 id="represent-api-canadian-electoral-data">Represent API (Canadian Electoral Data)<a class="headerlink" href="#represent-api-canadian-electoral-data" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>REPRESENT_API_URL</code></td>
<td><code>https://represent.opennorth.ca</code></td>
<td>OpenNorth Represent API endpoint. Used for postal code &rarr; representative lookups. No API key required.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="nocodb-data-browser">NocoDB (Data Browser) <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span><a class="headerlink" href="#nocodb-data-browser" title="Permanent link">&para;</a></h2>
<p>Read-only database browser. Useful for inspecting data without SQL.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>NOCODB_V2_PORT</code> / <code>NOCODB_PORT</code></td>
<td><code>8091</code></td>
<td>Host port for the NocoDB web UI.</td>
</tr>
<tr>
<td><code>NOCODB_URL</code></td>
<td><code>http://changemaker-v2-nocodb:8080</code></td>
<td>Internal Docker URL.</td>
</tr>
<tr>
<td><code>NC_ADMIN_EMAIL</code></td>
<td><code>admin@cmlite.org</code></td>
<td>NocoDB admin email.</td>
</tr>
<tr>
<td><code>NC_ADMIN_PASSWORD</code></td>
<td>&mdash;</td>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span> NocoDB admin password.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="media-manager">Media Manager <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span><a class="headerlink" href="#media-manager" title="Permanent link">&para;</a></h2>
<p>Video library with upload, analytics, scheduling, and a public gallery.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ENABLE_MEDIA_FEATURES</code></td>
<td><code>false</code></td>
<td><span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span> Set to <code>true</code> to enable the media system.</td>
</tr>
<tr>
<td><code>MEDIA_API_PORT</code></td>
<td><code>4100</code></td>
<td>Fastify media API port.</td>
</tr>
<tr>
<td><code>MEDIA_API_PUBLIC_URL</code></td>
<td><code>http://media-api:4100</code></td>
<td>Internal URL for the media API container.</td>
</tr>
<tr>
<td><code>MEDIA_ROOT</code></td>
<td><code>/media/library</code></td>
<td>Path to the video library inside the container.</td>
</tr>
<tr>
<td><code>MEDIA_UPLOADS</code></td>
<td><code>/media/uploads</code></td>
<td>Path for upload processing.</td>
</tr>
<tr>
<td><code>MAX_UPLOAD_SIZE_GB</code></td>
<td><code>10</code></td>
<td>Maximum single-file upload size in gigabytes.</td>
</tr>
<tr>
<td><code>PUBLIC_MEDIA_PORT</code></td>
<td><code>3100</code></td>
<td>Public media gallery server port.</td>
</tr>
<tr>
<td><code>VIDEO_PLAYER_DEBUG</code></td>
<td><code>false</code></td>
<td>Enable verbose video player logging.</td>
</tr>
</tbody>
</table>
<details class="example">
<summary>Analytics &amp; scheduling settings</summary>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>VIDEO_ANALYTICS_RETENTION_DAYS</code></td>
<td><code>90</code></td>
<td>Days to retain analytics data. GDPR-compliant with IP hashing.</td>
</tr>
<tr>
<td><code>VIDEO_ANALYTICS_IP_HASHING_ENABLED</code></td>
<td><code>true</code></td>
<td>Hash viewer IPs for privacy.</td>
</tr>
<tr>
<td><code>VIDEO_SCHEDULE_DEFAULT_TIMEZONE</code></td>
<td><code>UTC</code></td>
<td>Default timezone for scheduled publishing.</td>
</tr>
<tr>
<td><code>VIDEO_SCHEDULE_NOTIFICATION_ENABLED</code></td>
<td><code>true</code></td>
<td>Notify on scheduled publish/unpublish.</td>
</tr>
<tr>
<td><code>VIDEO_PREVIEW_LINK_EXPIRY_HOURS</code></td>
<td><code>24</code></td>
<td>Preview link JWT expiry (hours).</td>
</tr>
</tbody>
</table>
</details>
<hr />
<h2 id="gitea-git-hosting">Gitea (Git Hosting) <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span><a class="headerlink" href="#gitea-git-hosting" title="Permanent link">&para;</a></h2>
<p>Self-hosted Git repository. Optional service.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>GITEA_URL</code></td>
<td><code>http://gitea-changemaker:3000</code></td>
<td>Internal container URL for Gitea.</td>
</tr>
<tr>
<td><code>GITEA_PORT</code> / <code>GITEA_WEB_PORT</code></td>
<td><code>3030</code></td>
<td>Gitea web UI port.</td>
</tr>
<tr>
<td><code>GITEA_SSH_PORT</code></td>
<td><code>2222</code></td>
<td>Gitea SSH port for git operations.</td>
</tr>
<tr>
<td><code>GITEA_DB_TYPE</code></td>
<td><code>mysql</code></td>
<td>Database type (Gitea uses its own MySQL).</td>
</tr>
<tr>
<td><code>GITEA_DB_HOST</code></td>
<td><code>gitea-db:3306</code></td>
<td>Internal database host.</td>
</tr>
<tr>
<td><code>GITEA_DB_NAME</code></td>
<td><code>gitea</code></td>
<td>Database name.</td>
</tr>
<tr>
<td><code>GITEA_DB_USER</code></td>
<td><code>gitea</code></td>
<td>Database user.</td>
</tr>
<tr>
<td><code>GITEA_DB_PASSWD</code></td>
<td>&mdash;</td>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span> Gitea database password.</td>
</tr>
<tr>
<td><code>GITEA_DB_ROOT_PASSWORD</code></td>
<td>&mdash;</td>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span> MySQL root password for Gitea.</td>
</tr>
<tr>
<td><code>GITEA_ROOT_URL</code></td>
<td><code>https://git.cmlite.org</code></td>
<td>Public-facing URL for Gitea.</td>
</tr>
<tr>
<td><code>GITEA_DOMAIN</code></td>
<td><code>git.cmlite.org</code></td>
<td>Domain used in git clone URLs.</td>
</tr>
</tbody>
</table>
<details class="example">
<summary>Gitea Docs Comments</summary>
<p>Enable comments on MkDocs documentation pages, backed by Gitea Issues.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>GITEA_COMMENTS_ENABLED</code></td>
<td><code>false</code></td>
<td><span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span> Enable comments on MkDocs pages.</td>
</tr>
<tr>
<td><code>GITEA_API_TOKEN</code></td>
<td><em>(empty)</em></td>
<td>Personal access token with repo write scope. Create in Gitea &rarr; Settings &rarr; Applications.</td>
</tr>
<tr>
<td><code>GITEA_COMMENTS_REPO_OWNER</code></td>
<td><em>(empty)</em></td>
<td>Gitea username that owns the docs-comments repo.</td>
</tr>
<tr>
<td><code>GITEA_COMMENTS_REPO_NAME</code></td>
<td><code>docs-comments</code></td>
<td>Repository name (auto-created via admin setup).</td>
</tr>
<tr>
<td><code>GITEA_OAUTH_CLIENT_ID</code></td>
<td><em>(empty)</em></td>
<td>OAuth2 application client ID (create in Gitea &rarr; Settings &rarr; Applications &rarr; OAuth2).</td>
</tr>
<tr>
<td><code>GITEA_OAUTH_CLIENT_SECRET</code></td>
<td><em>(empty)</em></td>
<td>OAuth2 application client secret.</td>
</tr>
</tbody>
</table>
</details>
<hr />
<h2 id="n8n-workflow-automation">n8n (Workflow Automation) <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span><a class="headerlink" href="#n8n-workflow-automation" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>N8N_PORT</code></td>
<td><code>5678</code></td>
<td>n8n web UI port.</td>
</tr>
<tr>
<td><code>N8N_HOST</code></td>
<td><code>n8n.cmlite.org</code></td>
<td>Public hostname for n8n.</td>
</tr>
<tr>
<td><code>N8N_ENCRYPTION_KEY</code></td>
<td>&mdash;</td>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span> Encryption key for n8n credentials storage.</td>
</tr>
<tr>
<td><code>N8N_USER_EMAIL</code></td>
<td><code>admin@example.com</code></td>
<td>Initial n8n admin email.</td>
</tr>
<tr>
<td><code>N8N_USER_PASSWORD</code></td>
<td>&mdash;</td>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span> Initial n8n admin password.</td>
</tr>
<tr>
<td><code>GENERIC_TIMEZONE</code></td>
<td><code>UTC</code></td>
<td>Timezone for n8n cron triggers.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="mkdocs-documentation">MkDocs (Documentation)<a class="headerlink" href="#mkdocs-documentation" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>MKDOCS_PORT</code></td>
<td><code>4003</code></td>
<td>MkDocs dev server port (live preview).</td>
</tr>
<tr>
<td><code>MKDOCS_SITE_SERVER_PORT</code></td>
<td><code>4004</code></td>
<td>MkDocs static site server port.</td>
</tr>
<tr>
<td><code>BASE_DOMAIN</code></td>
<td><code>https://cmlite.org</code></td>
<td>Base URL for generated documentation links.</td>
</tr>
<tr>
<td><code>MKDOCS_PREVIEW_URL</code></td>
<td><code>http://mkdocs:8000</code></td>
<td>Internal container URL.</td>
</tr>
<tr>
<td><code>MKDOCS_DOCS_PATH</code></td>
<td><code>/mkdocs/docs</code></td>
<td>Documentation source directory inside the container.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="code-server-web-ide">Code Server (Web IDE)<a class="headerlink" href="#code-server-web-ide" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>CODE_SERVER_PORT</code></td>
<td><code>8888</code></td>
<td>Code Server web UI port.</td>
</tr>
<tr>
<td><code>CODE_SERVER_URL</code></td>
<td><code>http://code-server-changemaker:8443</code></td>
<td>Internal container URL.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="homepage-service-dashboard">Homepage (Service Dashboard)<a class="headerlink" href="#homepage-service-dashboard" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>HOMEPAGE_PORT</code></td>
<td><code>3010</code></td>
<td>Homepage web UI port.</td>
</tr>
<tr>
<td><code>HOMEPAGE_EMBED_PORT</code></td>
<td><code>8887</code></td>
<td>Port for iframe embedding in admin.</td>
</tr>
<tr>
<td><code>HOMEPAGE_VAR_BASE_URL</code></td>
<td><code>http://localhost</code></td>
<td>Base URL used in Homepage service links.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="mini-qr-qr-code-generator">Mini QR (QR Code Generator)<a class="headerlink" href="#mini-qr-qr-code-generator" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>MINI_QR_PORT</code></td>
<td><code>8089</code></td>
<td>Mini QR direct access port.</td>
</tr>
<tr>
<td><code>MINI_QR_URL</code></td>
<td><code>http://mini-qr:8080</code></td>
<td>Internal container URL.</td>
</tr>
<tr>
<td><code>MINI_QR_EMBED_PORT</code></td>
<td><code>8885</code></td>
<td>Port for iframe embedding (walk sheets, cut exports).</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="excalidraw-whiteboard">Excalidraw (Whiteboard)<a class="headerlink" href="#excalidraw-whiteboard" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>EXCALIDRAW_PORT</code></td>
<td><code>8090</code></td>
<td>Excalidraw web UI port.</td>
</tr>
<tr>
<td><code>EXCALIDRAW_URL</code></td>
<td><code>http://excalidraw-changemaker:80</code></td>
<td>Internal container URL.</td>
</tr>
<tr>
<td><code>EXCALIDRAW_EMBED_PORT</code></td>
<td><code>8886</code></td>
<td>Port for iframe embedding.</td>
</tr>
<tr>
<td><code>EXCALIDRAW_WS_URL</code></td>
<td><code>wss://draw.cmlite.org</code></td>
<td>WebSocket URL for real-time collaboration.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="vaultwarden-password-manager">Vaultwarden (Password Manager) <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span><a class="headerlink" href="#vaultwarden-password-manager" title="Permanent link">&para;</a></h2>
<p>Self-hosted Bitwarden-compatible password manager. Optional service.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>VAULTWARDEN_PORT</code></td>
<td><code>8445</code></td>
<td>Vaultwarden web UI port.</td>
</tr>
<tr>
<td><code>VAULTWARDEN_URL</code></td>
<td><code>http://vaultwarden-changemaker:80</code></td>
<td>Internal container URL.</td>
</tr>
<tr>
<td><code>VAULTWARDEN_EMBED_PORT</code></td>
<td><code>8890</code></td>
<td>Port for iframe embedding in admin.</td>
</tr>
<tr>
<td><code>VAULTWARDEN_ADMIN_TOKEN</code></td>
<td><em>(empty)</em></td>
<td>Admin panel token (access at <code>/admin</code>). Generate with <code>openssl rand -hex 32</code>.</td>
</tr>
<tr>
<td><code>VAULTWARDEN_DOMAIN</code></td>
<td><code>https://vault.cmlite.org</code></td>
<td>Public-facing URL. <strong>Must use HTTPS</strong> &mdash; Bitwarden web vault enforces HTTPS for account creation. Set to your Pangolin tunnel URL.</td>
</tr>
<tr>
<td><code>VAULTWARDEN_SIGNUPS_ALLOWED</code></td>
<td><code>false</code></td>
<td>Allow new user self-registration. Keep <code>false</code> and use admin panel invites.</td>
</tr>
<tr>
<td><code>VAULTWARDEN_WEBSOCKET_ENABLED</code></td>
<td><code>true</code></td>
<td>Enable WebSocket notifications for real-time sync.</td>
</tr>
<tr>
<td><code>VAULTWARDEN_SMTP_SECURITY</code></td>
<td><code>off</code></td>
<td>SMTP security mode: <code>off</code> for MailHog, <code>starttls</code> or <code>force_tls</code> for production. Uses the main <code>SMTP_*</code> variables for host/credentials.</td>
</tr>
</tbody>
</table>
<div class="admonition info">
<p class="admonition-title">Initial setup</p>
<p>The <code>vaultwarden-init</code> container automatically invites the <code>INITIAL_ADMIN_EMAIL</code> user when starting. Check MailHog (or your SMTP) for the invitation email.</p>
</div>
<hr />
<h2 id="rocketchat-team-chat">Rocket.Chat (Team Chat) <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span><a class="headerlink" href="#rocketchat-team-chat" title="Permanent link">&para;</a></h2>
<p>Self-hosted team chat for volunteer coordination. Requires MongoDB (auto-configured).</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ENABLE_CHAT</code></td>
<td><code>false</code></td>
<td><span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span> Set to <code>true</code> to enable the Rocket.Chat integration. The initial default; once saved in admin Settings, the DB value is authoritative.</td>
</tr>
<tr>
<td><code>ROCKETCHAT_ADMIN_USER</code></td>
<td><code>rcadmin</code></td>
<td>Rocket.Chat admin username.</td>
</tr>
<tr>
<td><code>ROCKETCHAT_ADMIN_PASSWORD</code></td>
<td>&mdash;</td>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span> Rocket.Chat admin password.</td>
</tr>
<tr>
<td><code>ROCKETCHAT_URL</code></td>
<td><code>http://rocketchat-changemaker:3000</code></td>
<td>Internal container URL.</td>
</tr>
<tr>
<td><code>ROCKETCHAT_EMBED_PORT</code></td>
<td><code>8891</code></td>
<td>Port for iframe embedding in admin.</td>
</tr>
<tr>
<td><code>MONGO_ROOT_USER</code></td>
<td><code>rocketchat</code></td>
<td>MongoDB admin username.</td>
</tr>
<tr>
<td><code>MONGO_ROOT_PASSWORD</code></td>
<td>&mdash;</td>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span> MongoDB admin password. MongoDB runs with <code>--auth</code> enabled.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="gancio-event-management">Gancio (Event Management) <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span><a class="headerlink" href="#gancio-event-management" title="Permanent link">&para;</a></h2>
<p>Self-hosted event management platform. Uses the shared PostgreSQL database (auto-created by <code>init-gancio-db.sh</code>).</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>GANCIO_PORT</code></td>
<td><code>8092</code></td>
<td>Gancio web UI port.</td>
</tr>
<tr>
<td><code>GANCIO_URL</code></td>
<td><code>http://gancio-changemaker:13120</code></td>
<td>Internal container URL.</td>
</tr>
<tr>
<td><code>GANCIO_EMBED_PORT</code></td>
<td><code>8892</code></td>
<td>Port for iframe embedding in admin.</td>
</tr>
<tr>
<td><code>GANCIO_BASE_URL</code></td>
<td><code>https://events.cmlite.org</code></td>
<td>Public-facing URL for Gancio. Used in event links.</td>
</tr>
<tr>
<td><code>GANCIO_ADMIN_USER</code></td>
<td><code>admin</code></td>
<td>Gancio admin username for shift-to-event sync (OAuth login).</td>
</tr>
<tr>
<td><code>GANCIO_ADMIN_PASSWORD</code></td>
<td>&mdash;</td>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span> Gancio admin password.</td>
</tr>
<tr>
<td><code>GANCIO_SYNC_ENABLED</code></td>
<td><code>false</code></td>
<td><span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span> Set to <code>true</code> to enable automatic shift &rarr; Gancio event synchronization.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="jitsi-meet-video-conferencing">Jitsi Meet (Video Conferencing) <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span><a class="headerlink" href="#jitsi-meet-video-conferencing" title="Permanent link">&para;</a></h2>
<p>Self-hosted video conferencing with JWT authentication. Integrates with Rocket.Chat for in-channel video calls.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ENABLE_MEET</code></td>
<td><code>false</code></td>
<td><span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span> Set to <code>true</code> to enable the Jitsi Meet integration. The initial default; once saved in admin Settings, the DB value is authoritative.</td>
</tr>
<tr>
<td><code>JITSI_APP_ID</code></td>
<td><code>changemaker</code></td>
<td>JWT application ID. Must match across Jitsi Prosody, Rocket.Chat app settings, and <code>JWT_ACCEPTED_ISSUERS</code>/<code>JWT_ACCEPTED_AUDIENCES</code>.</td>
</tr>
<tr>
<td><code>JITSI_APP_SECRET</code></td>
<td>&mdash;</td>
<td><span class="twemoji text-red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 13h-2V7h2m0 10h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg></span> JWT secret for signing Jitsi tokens. Generate with <code>openssl rand -hex 32</code>. Shared between Jitsi Prosody, Rocket.Chat, and the API.</td>
</tr>
<tr>
<td><code>JITSI_JICOFO_AUTH_PASSWORD</code></td>
<td>&mdash;</td>
<td>Internal XMPP password for Jicofo (conference focus). Generate with <code>openssl rand -hex 16</code>.</td>
</tr>
<tr>
<td><code>JITSI_JVB_AUTH_PASSWORD</code></td>
<td>&mdash;</td>
<td>Internal XMPP password for JVB (video bridge). Generate with <code>openssl rand -hex 16</code>.</td>
</tr>
<tr>
<td><code>JITSI_EMBED_PORT</code></td>
<td><code>8893</code></td>
<td>Port for iframe embedding in admin.</td>
</tr>
<tr>
<td><code>JITSI_URL</code></td>
<td><code>http://jitsi-web-changemaker:80</code></td>
<td>Internal container URL.</td>
</tr>
<tr>
<td><code>JVB_ADVERTISE_IP</code></td>
<td><em>(empty)</em></td>
<td>Server's public IP address. <strong>Required in production</strong> for NAT traversal so remote participants can connect.</td>
</tr>
<tr>
<td><code>JVB_PORT</code></td>
<td><code>10000</code></td>
<td>UDP port for media traffic. Must be open in your firewall.</td>
</tr>
</tbody>
</table>
<div class="admonition warning">
<p class="admonition-title">Production requirements</p>
<ul>
<li><code>JVB_ADVERTISE_IP</code> must be set to your server's public IP for calls to work outside the local network.</li>
<li>Port <code>10000/udp</code> must be open in your firewall for media traffic.</li>
<li>Calls must go through the production domain (not localhost) for SSL/JWT to work.</li>
</ul>
</div>
<hr />
<h2 id="sms-campaigns-termux-android-bridge">SMS Campaigns (Termux Android Bridge) <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span><a class="headerlink" href="#sms-campaigns-termux-android-bridge" title="Permanent link">&para;</a></h2>
<p>Send SMS messages via an Android phone running the Termux API server. The phone acts as an SMS gateway.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ENABLE_SMS</code></td>
<td><code>false</code></td>
<td><span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span> Set to <code>true</code> to enable SMS campaigns. The initial default; once saved in admin Settings, the DB value is authoritative.</td>
</tr>
<tr>
<td><code>TERMUX_API_URL</code></td>
<td><code>http://10.0.0.193:5001</code></td>
<td>URL of the Termux API server running on the Android phone.</td>
</tr>
<tr>
<td><code>TERMUX_API_KEY</code></td>
<td><em>(empty)</em></td>
<td>API key for authenticating with the Termux server (HMAC auth via <code>X-API-Key</code> header).</td>
</tr>
<tr>
<td><code>SMS_DELAY_BETWEEN_MS</code></td>
<td><code>3000</code></td>
<td>Delay between sending individual SMS messages (ms). Prevents carrier throttling.</td>
</tr>
<tr>
<td><code>SMS_MAX_RETRIES</code></td>
<td><code>3</code></td>
<td>Maximum retry attempts for failed SMS sends.</td>
</tr>
<tr>
<td><code>SMS_RESPONSE_SYNC_INTERVAL_MS</code></td>
<td><code>30000</code></td>
<td>How often to poll the phone's inbox for responses (ms).</td>
</tr>
<tr>
<td><code>SMS_DEVICE_MONITOR_INTERVAL_MS</code></td>
<td><code>30000</code></td>
<td>How often to check device health &mdash; battery, connectivity (ms).</td>
</tr>
</tbody>
</table>
<div class="admonition tip">
<p class="admonition-title">GUI configuration</p>
<p>The Termux API URL and API key can also be configured from <strong>Admin &rarr; Settings &rarr; SMS</strong>. Database values override these env vars when set.</p>
</div>
<hr />
<h2 id="mailhog-development-email">MailHog (Development Email)<a class="headerlink" href="#mailhog-development-email" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>MAILHOG_SMTP_PORT</code></td>
<td><code>1025</code></td>
<td>SMTP port for capturing emails.</td>
</tr>
<tr>
<td><code>MAILHOG_WEB_PORT</code></td>
<td><code>8025</code></td>
<td>Web UI to view captured emails.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="nar-national-address-register">NAR (National Address Register) <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span><a class="headerlink" href="#nar-national-address-register" title="Permanent link">&para;</a></h2>
<p>Canadian address data import for geographic canvassing.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>NAR_DATA_DIR</code></td>
<td><code>/data</code></td>
<td>Path to extracted NAR data inside the container. Expects <code>YYYYMM/Addresses/</code> and <code>YYYYMM/Locations/</code> subdirectories. Mount via <code>./data:/data:ro</code> in Docker Compose.</td>
</tr>
</tbody>
</table>
<p>Download NAR data from <a href="https://www150.statcan.gc.ca/n1/pub/46-26-0002/462600022022001-eng.htm">Statistics Canada</a>.</p>
<hr />
<h2 id="geocoding">Geocoding <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span><a class="headerlink" href="#geocoding" title="Permanent link">&para;</a></h2>
<p>Multi-provider geocoding for address resolution. Works out of the box with free providers; optional paid providers improve accuracy.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>MAPBOX_API_KEY</code></td>
<td><em>(empty)</em></td>
<td>Mapbox API key for improved geocoding accuracy. Free tier: 100k requests/month. <a href="https://www.mapbox.com/pricing">Sign up</a>.</td>
</tr>
<tr>
<td><code>GEOCODING_RATE_LIMIT_MS</code></td>
<td><code>1100</code></td>
<td>Delay between requests to free providers (ms). Respects rate limits.</td>
</tr>
<tr>
<td><code>GEOCODING_CACHE_ENABLED</code></td>
<td><code>true</code></td>
<td>Enable Redis-backed geocoding cache.</td>
</tr>
<tr>
<td><code>GEOCODING_CACHE_TTL_HOURS</code></td>
<td><code>24</code></td>
<td>Cache lifetime in hours.</td>
</tr>
<tr>
<td><code>GOOGLE_MAPS_API_KEY</code></td>
<td><em>(empty)</em></td>
<td>Google Maps API key. Most accurate but $0.005/request after free tier.</td>
</tr>
<tr>
<td><code>GOOGLE_MAPS_ENABLED</code></td>
<td><code>false</code></td>
<td>Enable Google Maps as a geocoding provider.</td>
</tr>
<tr>
<td><code>GEOCODING_PARALLEL_ENABLED</code></td>
<td><code>true</code></td>
<td>Enable parallel geocoding for bulk imports (~10x speedup).</td>
</tr>
<tr>
<td><code>GEOCODING_BATCH_SIZE</code></td>
<td><code>10</code></td>
<td>Number of concurrent geocoding requests during bulk operations.</td>
</tr>
<tr>
<td><code>BULK_GEOCODE_ENABLED</code></td>
<td><code>true</code></td>
<td>Enable bulk re-geocoding from the admin UI.</td>
</tr>
<tr>
<td><code>BULK_GEOCODE_MAX_BATCH</code></td>
<td><code>5000</code></td>
<td>Maximum locations per bulk geocoding run.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="overpass-area-import">Overpass / Area Import <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span><a class="headerlink" href="#overpass-area-import" title="Permanent link">&para;</a></h2>
<p>OpenStreetMap data import for map enrichment.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>OVERPASS_API_URL</code></td>
<td><code>https://overpass-api.de/api/interpreter</code></td>
<td>Overpass API endpoint. Use a private instance for heavy usage.</td>
</tr>
<tr>
<td><code>OVERPASS_MIN_DELAY_MS</code></td>
<td><code>30000</code></td>
<td>Minimum delay between requests (ms). The public API requires 30 seconds.</td>
</tr>
<tr>
<td><code>AREA_IMPORT_MAX_GRID_POINTS</code></td>
<td><code>500</code></td>
<td>Maximum reverse-geocode grid points per area import.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="pangolin-tunnel">Pangolin Tunnel <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span><a class="headerlink" href="#pangolin-tunnel" title="Permanent link">&para;</a></h2>
<p>Expose services to the internet without port forwarding, using a self-hosted <a href="https://github.com/fosrl/pangolin">Pangolin</a> instance.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>PANGOLIN_API_URL</code></td>
<td><code>https://api.bnkserve.org/v1</code></td>
<td>Pangolin server API endpoint.</td>
</tr>
<tr>
<td><code>PANGOLIN_API_KEY</code></td>
<td><em>(empty)</em></td>
<td>API key for Pangolin management.</td>
</tr>
<tr>
<td><code>PANGOLIN_ORG_ID</code></td>
<td><em>(empty)</em></td>
<td>Organization ID in Pangolin.</td>
</tr>
<tr>
<td><code>PANGOLIN_SITE_ID</code></td>
<td><em>(empty)</em></td>
<td>Site ID (populated after setup via admin GUI).</td>
</tr>
<tr>
<td><code>PANGOLIN_ENDPOINT</code></td>
<td><code>https://pangolin.bnkserve.org</code></td>
<td>Pangolin tunnel endpoint.</td>
</tr>
<tr>
<td><code>PANGOLIN_NEWT_ID</code></td>
<td><em>(empty)</em></td>
<td>Newt client ID (populated after setup).</td>
</tr>
<tr>
<td><code>PANGOLIN_NEWT_SECRET</code></td>
<td><em>(empty)</em></td>
<td>Newt client secret (populated after setup).</td>
</tr>
</tbody>
</table>
<div class="admonition tip">
<p class="admonition-title">Setup flow</p>
<p>Configure the tunnel from <strong>Admin &rarr; Settings &rarr; Pangolin</strong>. The setup wizard walks you through creating a site, copying credentials, and connecting the Newt container. See <a href="../../deployment/">Deployment</a> for the full guide.</p>
</div>
<hr />
<h2 id="monitoring">Monitoring <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span><a class="headerlink" href="#monitoring" title="Permanent link">&para;</a></h2>
<p>These services are behind the <code>monitoring</code> Docker Compose profile. Start them with:</p>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-3-1"><a id="__codelineno-3-1" name="__codelineno-3-1" href="#__codelineno-3-1"></a>docker<span class="w"> </span>compose<span class="w"> </span>--profile<span class="w"> </span>monitoring<span class="w"> </span>up<span class="w"> </span>-d
</span></code></pre></div>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>PROMETHEUS_PORT</code></td>
<td><code>9090</code></td>
<td>Prometheus web UI / query port.</td>
</tr>
<tr>
<td><code>GRAFANA_PORT</code></td>
<td><code>3005</code></td>
<td>Grafana dashboard port.</td>
</tr>
<tr>
<td><code>GRAFANA_ADMIN_PASSWORD</code></td>
<td><code>admin</code></td>
<td><span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span> Change in production.</td>
</tr>
<tr>
<td><code>GRAFANA_ROOT_URL</code></td>
<td><code>http://localhost:3005</code></td>
<td>Public URL for Grafana (used in links).</td>
</tr>
<tr>
<td><code>CADVISOR_PORT</code></td>
<td><code>8086</code></td>
<td>cAdvisor container metrics port.</td>
</tr>
<tr>
<td><code>NODE_EXPORTER_PORT</code></td>
<td><code>9100</code></td>
<td>Prometheus node exporter port.</td>
</tr>
<tr>
<td><code>REDIS_EXPORTER_PORT</code></td>
<td><code>9121</code></td>
<td>Redis metrics exporter port.</td>
</tr>
<tr>
<td><code>ALERTMANAGER_PORT</code></td>
<td><code>9093</code></td>
<td>Alertmanager web UI port.</td>
</tr>
<tr>
<td><code>GOTIFY_PORT</code></td>
<td><code>8889</code></td>
<td>Gotify push notification port.</td>
</tr>
<tr>
<td><code>GOTIFY_ADMIN_USER</code></td>
<td><code>admin</code></td>
<td>Gotify admin username.</td>
</tr>
<tr>
<td><code>GOTIFY_ADMIN_PASSWORD</code></td>
<td><code>admin</code></td>
<td><span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span> Change in production.</td>
</tr>
<tr>
<td><code>GRAFANA_EMBED_PORT</code></td>
<td><code>8894</code></td>
<td>Port for iframe embedding Grafana in admin.</td>
</tr>
<tr>
<td><code>ALERTMANAGER_EMBED_PORT</code></td>
<td><code>8895</code></td>
<td>Port for iframe embedding Alertmanager in admin.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="bunker-ops-fleet-management">Bunker Ops (Fleet Management) <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span><a class="headerlink" href="#bunker-ops-fleet-management" title="Permanent link">&para;</a></h2>
<p>Remote metrics push for managing multiple Changemaker Lite instances from a central monitoring server.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>INSTANCE_LABEL</code></td>
<td><em>(empty)</em></td>
<td>Unique label for this instance (used as a Prometheus metric label). Falls back to <code>DOMAIN</code> if empty.</td>
</tr>
<tr>
<td><code>BUNKER_OPS_ENABLED</code></td>
<td><code>false</code></td>
<td><span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span> Enable remote metrics push to a central VictoriaMetrics server.</td>
</tr>
<tr>
<td><code>BUNKER_OPS_REMOTE_WRITE_URL</code></td>
<td><em>(empty)</em></td>
<td>VictoriaMetrics <code>remote_write</code> endpoint (e.g., <code>https://ops.example.com/api/v1/write</code>).</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="social-people-analytics">Social, People &amp; Analytics <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span><a class="headerlink" href="#social-people-analytics" title="Permanent link">&para;</a></h2>
<p>Feature flags for the social graph, CRM people module, and analytics dashboard.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ENABLE_SOCIAL</code></td>
<td><code>false</code></td>
<td><span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span> Enable the social module (friendships, challenges, spotlights, referrals). The initial default; once saved in admin Settings, the DB value is authoritative.</td>
</tr>
<tr>
<td><code>ENABLE_PEOPLE</code></td>
<td><code>false</code></td>
<td><span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span> Enable the CRM people module. The initial default; once saved in admin Settings, the DB value is authoritative.</td>
</tr>
<tr>
<td><code>ENABLE_ANALYTICS</code></td>
<td><code>false</code></td>
<td><span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span> Enable the analytics dashboard with visitor tracking and geographic insights. The initial default; once saved in admin Settings, the DB value is authoritative.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="geoip-maxmind-geolite2">GeoIP (MaxMind GeoLite2) <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span><a class="headerlink" href="#geoip-maxmind-geolite2" title="Permanent link">&para;</a></h2>
<p>Geographic IP lookup for analytics visitor location tracking. Requires a free MaxMind account.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>MAXMIND_ACCOUNT_ID</code></td>
<td><em>(empty)</em></td>
<td>MaxMind account ID. <a href="https://www.maxmind.com/en/geolite2/signup">Sign up free</a>.</td>
</tr>
<tr>
<td><code>MAXMIND_LICENSE_KEY</code></td>
<td><em>(empty)</em></td>
<td>MaxMind license key. When set, the GeoLite2-City database auto-downloads at startup.</td>
</tr>
<tr>
<td><code>GEOIP_DB_PATH</code></td>
<td><code>/data/geoip/GeoLite2-City.mmdb</code></td>
<td>Path to the GeoLite2 database file inside the container.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="control-panel-agent-ccp">Control Panel Agent (CCP) <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span><a class="headerlink" href="#control-panel-agent-ccp" title="Permanent link">&para;</a></h2>
<p>Remote management agent for the Changemaker Control Panel &mdash; enables centralized multi-instance management.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ENABLE_CCP_AGENT</code></td>
<td><code>false</code></td>
<td><span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 22a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3zm-1-3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57l-2.29-3.96L14 17l-5.07-5.07-3.75 6.5c-.11.16-.18.36-.18.57m8-9a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1"/></svg></span> Enable the CCP remote management agent.</td>
</tr>
<tr>
<td><code>CCP_URL</code></td>
<td><em>(empty)</em></td>
<td>URL of the Changemaker Control Panel server.</td>
</tr>
<tr>
<td><code>CCP_INVITE_CODE</code></td>
<td><em>(empty)</em></td>
<td>One-time invite code for agent registration with the control panel.</td>
</tr>
<tr>
<td><code>CCP_AGENT_URL</code></td>
<td><em>(empty)</em></td>
<td>How the CCP can reach this agent (must be externally accessible).</td>
</tr>
<tr>
<td><code>CCP_AGENT_PORT</code></td>
<td><code>7443</code></td>
<td>Agent listener port.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="container-registry">Container Registry <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span><a class="headerlink" href="#container-registry" title="Permanent link">&para;</a></h2>
<p>Settings for pulling pre-built production images from the Gitea container registry.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>GITEA_REGISTRY</code></td>
<td><code>gitea.bnkops.com/admin</code></td>
<td>Registry hostname and namespace for pulling images.</td>
</tr>
<tr>
<td><code>IMAGE_TAG</code></td>
<td><em>(empty)</em></td>
<td>Image tag to pull. Set to a commit SHA or <code>latest</code> for pre-built images. Leave empty (defaults to <code>local</code>) to build from source.</td>
</tr>
<tr>
<td><code>COMPOSE_PROFILES</code></td>
<td><em>(empty)</em></td>
<td>Docker Compose profiles to activate. Set to <code>monitoring</code> to include Prometheus/Grafana/Alertmanager in every <code>docker compose up -d</code>.</td>
</tr>
<tr>
<td><code>GITEA_REGISTRY_USER</code></td>
<td><code>admin</code></td>
<td>Registry username for <code>docker login</code> and the registry status API endpoint.</td>
</tr>
<tr>
<td><code>GITEA_REGISTRY_PASS</code></td>
<td><em>(empty)</em></td>
<td>Registry password for the status API endpoint. For <code>docker push/pull</code>, use <code>docker login gitea.bnkops.com</code>.</td>
</tr>
<tr>
<td><code>GITEA_REGISTRY_API_TOKEN</code></td>
<td><em>(empty)</em></td>
<td>API token for the <strong>remote</strong> registry (gitea.bnkops.com). Used by <code>build-release.sh --upload</code> to publish release tarballs. Create at Gitea &rarr; User Settings &rarr; Applications. <strong>Not</strong> the same as <code>GITEA_API_TOKEN</code>.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="docker-container-management">Docker / Container Management <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span><a class="headerlink" href="#docker-container-management" title="Permanent link">&para;</a></h2>
<p>Internal settings for the admin dashboard's service status panel and container management.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>DOCKER_NETWORK_NAME</code></td>
<td><code>changemaker-lite</code></td>
<td>Docker bridge network name. Used by the dashboard to auto-discover containers.</td>
</tr>
<tr>
<td><code>DOCKER_PROXY_URL</code></td>
<td><code>http://docker-socket-proxy:2375</code></td>
<td>Read-only Docker socket proxy URL for container inspection.</td>
</tr>
<tr>
<td><code>NEWT_CONTAINER_NAME</code></td>
<td><code>newt-changemaker</code></td>
<td>Newt tunnel container name (for restart/status checks).</td>
</tr>
<tr>
<td><code>NEWT_COMPOSE_SERVICE</code></td>
<td><code>newt</code></td>
<td>Docker Compose service name for the Newt container.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="embed-proxy-ports">Embed Proxy Ports <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span><a class="headerlink" href="#embed-proxy-ports" title="Permanent link">&para;</a></h2>
<p>Dedicated nginx ports for iframe embedding services in the admin dashboard without requiring DNS/subdomains. Change these to avoid port conflicts when running multiple instances on one host.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>NOCODB_EMBED_PORT</code></td>
<td><code>8881</code></td>
<td>NocoDB iframe port.</td>
</tr>
<tr>
<td><code>N8N_EMBED_PORT</code></td>
<td><code>8882</code></td>
<td>n8n iframe port.</td>
</tr>
<tr>
<td><code>GITEA_EMBED_PORT</code></td>
<td><code>8883</code></td>
<td>Gitea iframe port.</td>
</tr>
<tr>
<td><code>MAILHOG_EMBED_PORT</code></td>
<td><code>8884</code></td>
<td>MailHog iframe port.</td>
</tr>
<tr>
<td><code>MINI_QR_EMBED_PORT</code></td>
<td><code>8885</code></td>
<td>Mini QR iframe port.</td>
</tr>
<tr>
<td><code>EXCALIDRAW_EMBED_PORT</code></td>
<td><code>8886</code></td>
<td>Excalidraw iframe port.</td>
</tr>
<tr>
<td><code>HOMEPAGE_EMBED_PORT</code></td>
<td><code>8887</code></td>
<td>Homepage iframe port.</td>
</tr>
<tr>
<td><code>VAULTWARDEN_EMBED_PORT</code></td>
<td><code>8890</code></td>
<td>Vaultwarden iframe port.</td>
</tr>
<tr>
<td><code>ROCKETCHAT_EMBED_PORT</code></td>
<td><code>8891</code></td>
<td>Rocket.Chat iframe port.</td>
</tr>
<tr>
<td><code>GANCIO_EMBED_PORT</code></td>
<td><code>8892</code></td>
<td>Gancio iframe port.</td>
</tr>
<tr>
<td><code>JITSI_EMBED_PORT</code></td>
<td><code>8893</code></td>
<td>Jitsi iframe port.</td>
</tr>
<tr>
<td><code>GRAFANA_EMBED_PORT</code></td>
<td><code>8894</code></td>
<td>Grafana iframe port.</td>
</tr>
<tr>
<td><code>ALERTMANAGER_EMBED_PORT</code></td>
<td><code>8895</code></td>
<td>Alertmanager iframe port.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="gitea-docs-version-history">Gitea Docs Version History <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span><a class="headerlink" href="#gitea-docs-version-history" title="Permanent link">&para;</a></h2>
<p>Settings for the documentation version history feature (backed by Gitea repository commits).</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>GITEA_DOCS_REPO</code></td>
<td><code>admin/changemaker.lite</code></td>
<td>Gitea repository path for docs version history.</td>
</tr>
<tr>
<td><code>GITEA_DOCS_PREFIX</code></td>
<td><code>mkdocs/docs</code></td>
<td>Path prefix within the repository where documentation files live.</td>
</tr>
<tr>
<td><code>GITEA_DOCS_BRANCH</code></td>
<td><code>v2</code></td>
<td>Git branch to query for version history.</td>
</tr>
<tr>
<td><code>GITEA_ADMIN_PASSWORD</code></td>
<td><em>(empty)</em></td>
<td>Gitea admin password. Used <strong>once</strong> during initial setup to create an API token, then can be cleared.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="prisma-cli-host-side">Prisma CLI (Host-Side) <span class="twemoji"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"/></svg></span><a class="headerlink" href="#prisma-cli-host-side" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>DATABASE_URL</code></td>
<td><code>postgresql://changemaker:YOUR_POSTGRES_PASSWORD@localhost:5433/changemaker_v2</code></td>
<td>Full PostgreSQL connection string. <strong>Only used when running Prisma CLI on the host</strong> (<code>npx prisma migrate dev</code>). Docker containers resolve the database hostname internally via Docker Compose environment variables.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="generating-secrets">Generating Secrets<a class="headerlink" href="#generating-secrets" title="Permanent link">&para;</a></h2>
<p>Use these commands to generate all required secrets at once:</p>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-4-1"><a id="__codelineno-4-1" name="__codelineno-4-1" href="#__codelineno-4-1"></a><span class="c1"># JWT secrets (three separate values)</span>
</span><span id="__span-4-2"><a id="__codelineno-4-2" name="__codelineno-4-2" href="#__codelineno-4-2"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;JWT_ACCESS_SECRET=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">32</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-3"><a id="__codelineno-4-3" name="__codelineno-4-3" href="#__codelineno-4-3"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;JWT_REFRESH_SECRET=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">32</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-4"><a id="__codelineno-4-4" name="__codelineno-4-4" href="#__codelineno-4-4"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;JWT_INVITE_SECRET=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">32</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-5"><a id="__codelineno-4-5" name="__codelineno-4-5" href="#__codelineno-4-5"></a>
</span><span id="__span-4-6"><a id="__codelineno-4-6" name="__codelineno-4-6" href="#__codelineno-4-6"></a><span class="c1"># Encryption key (must differ from JWT secrets)</span>
</span><span id="__span-4-7"><a id="__codelineno-4-7" name="__codelineno-4-7" href="#__codelineno-4-7"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;ENCRYPTION_KEY=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">32</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-8"><a id="__codelineno-4-8" name="__codelineno-4-8" href="#__codelineno-4-8"></a>
</span><span id="__span-4-9"><a id="__codelineno-4-9" name="__codelineno-4-9" href="#__codelineno-4-9"></a><span class="c1"># Security extras (key separation)</span>
</span><span id="__span-4-10"><a id="__codelineno-4-10" name="__codelineno-4-10" href="#__codelineno-4-10"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;GITEA_SSO_SECRET=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">32</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-11"><a id="__codelineno-4-11" name="__codelineno-4-11" href="#__codelineno-4-11"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;SERVICE_PASSWORD_SALT=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">32</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-12"><a id="__codelineno-4-12" name="__codelineno-4-12" href="#__codelineno-4-12"></a>
</span><span id="__span-4-13"><a id="__codelineno-4-13" name="__codelineno-4-13" href="#__codelineno-4-13"></a><span class="c1"># Database and Redis passwords</span>
</span><span id="__span-4-14"><a id="__codelineno-4-14" name="__codelineno-4-14" href="#__codelineno-4-14"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;V2_POSTGRES_PASSWORD=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">24</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-15"><a id="__codelineno-4-15" name="__codelineno-4-15" href="#__codelineno-4-15"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;REDIS_PASSWORD=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">24</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-16"><a id="__codelineno-4-16" name="__codelineno-4-16" href="#__codelineno-4-16"></a>
</span><span id="__span-4-17"><a id="__codelineno-4-17" name="__codelineno-4-17" href="#__codelineno-4-17"></a><span class="c1"># Listmonk</span>
</span><span id="__span-4-18"><a id="__codelineno-4-18" name="__codelineno-4-18" href="#__codelineno-4-18"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;LISTMONK_DB_PASSWORD=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">24</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-19"><a id="__codelineno-4-19" name="__codelineno-4-19" href="#__codelineno-4-19"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;LISTMONK_WEB_ADMIN_PASSWORD=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">16</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-20"><a id="__codelineno-4-20" name="__codelineno-4-20" href="#__codelineno-4-20"></a><span class="nv">LISTMONK_TOKEN</span><span class="o">=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">16</span><span class="k">)</span>
</span><span id="__span-4-21"><a id="__codelineno-4-21" name="__codelineno-4-21" href="#__codelineno-4-21"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;LISTMONK_API_TOKEN=</span><span class="nv">$LISTMONK_TOKEN</span><span class="s2">&quot;</span>
</span><span id="__span-4-22"><a id="__codelineno-4-22" name="__codelineno-4-22" href="#__codelineno-4-22"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;LISTMONK_ADMIN_PASSWORD=</span><span class="nv">$LISTMONK_TOKEN</span><span class="s2">&quot;</span>
</span><span id="__span-4-23"><a id="__codelineno-4-23" name="__codelineno-4-23" href="#__codelineno-4-23"></a>
</span><span id="__span-4-24"><a id="__codelineno-4-24" name="__codelineno-4-24" href="#__codelineno-4-24"></a><span class="c1"># Supporting services</span>
</span><span id="__span-4-25"><a id="__codelineno-4-25" name="__codelineno-4-25" href="#__codelineno-4-25"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;GITEA_DB_PASSWD=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">24</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-26"><a id="__codelineno-4-26" name="__codelineno-4-26" href="#__codelineno-4-26"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;GITEA_DB_ROOT_PASSWORD=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">24</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-27"><a id="__codelineno-4-27" name="__codelineno-4-27" href="#__codelineno-4-27"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;N8N_ENCRYPTION_KEY=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">32</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-28"><a id="__codelineno-4-28" name="__codelineno-4-28" href="#__codelineno-4-28"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;N8N_USER_PASSWORD=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">16</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-29"><a id="__codelineno-4-29" name="__codelineno-4-29" href="#__codelineno-4-29"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;NC_ADMIN_PASSWORD=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">16</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-30"><a id="__codelineno-4-30" name="__codelineno-4-30" href="#__codelineno-4-30"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;INITIAL_ADMIN_PASSWORD=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-base64<span class="w"> </span><span class="m">18</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-31"><a id="__codelineno-4-31" name="__codelineno-4-31" href="#__codelineno-4-31"></a>
</span><span id="__span-4-32"><a id="__codelineno-4-32" name="__codelineno-4-32" href="#__codelineno-4-32"></a><span class="c1"># Vaultwarden</span>
</span><span id="__span-4-33"><a id="__codelineno-4-33" name="__codelineno-4-33" href="#__codelineno-4-33"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;VAULTWARDEN_ADMIN_TOKEN=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">32</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-34"><a id="__codelineno-4-34" name="__codelineno-4-34" href="#__codelineno-4-34"></a>
</span><span id="__span-4-35"><a id="__codelineno-4-35" name="__codelineno-4-35" href="#__codelineno-4-35"></a><span class="c1"># Rocket.Chat + MongoDB</span>
</span><span id="__span-4-36"><a id="__codelineno-4-36" name="__codelineno-4-36" href="#__codelineno-4-36"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;ROCKETCHAT_ADMIN_PASSWORD=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">16</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-37"><a id="__codelineno-4-37" name="__codelineno-4-37" href="#__codelineno-4-37"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;MONGO_ROOT_PASSWORD=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">24</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-38"><a id="__codelineno-4-38" name="__codelineno-4-38" href="#__codelineno-4-38"></a>
</span><span id="__span-4-39"><a id="__codelineno-4-39" name="__codelineno-4-39" href="#__codelineno-4-39"></a><span class="c1"># Gancio</span>
</span><span id="__span-4-40"><a id="__codelineno-4-40" name="__codelineno-4-40" href="#__codelineno-4-40"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;GANCIO_ADMIN_PASSWORD=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">16</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-41"><a id="__codelineno-4-41" name="__codelineno-4-41" href="#__codelineno-4-41"></a>
</span><span id="__span-4-42"><a id="__codelineno-4-42" name="__codelineno-4-42" href="#__codelineno-4-42"></a><span class="c1"># Jitsi Meet</span>
</span><span id="__span-4-43"><a id="__codelineno-4-43" name="__codelineno-4-43" href="#__codelineno-4-43"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;JITSI_APP_SECRET=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">32</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-44"><a id="__codelineno-4-44" name="__codelineno-4-44" href="#__codelineno-4-44"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;JITSI_JICOFO_AUTH_PASSWORD=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">16</span><span class="k">)</span><span class="s2">&quot;</span>
</span><span id="__span-4-45"><a id="__codelineno-4-45" name="__codelineno-4-45" href="#__codelineno-4-45"></a><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;JITSI_JVB_AUTH_PASSWORD=</span><span class="k">$(</span>openssl<span class="w"> </span>rand<span class="w"> </span>-hex<span class="w"> </span><span class="m">16</span><span class="k">)</span><span class="s2">&quot;</span>
</span></code></pre></div>
<div class="admonition tip">
<p class="admonition-title">Tip</p>
<p>Copy the output and paste the values into your <code>.env</code> file. The <code>INITIAL_ADMIN_PASSWORD</code> uses base64 encoding to ensure it contains uppercase, lowercase, and digits (meeting the password policy).</p>
</div>
<hr />
<h2 id="minimal-vs-full-deployment">Minimal vs Full Deployment<a class="headerlink" href="#minimal-vs-full-deployment" title="Permanent link">&para;</a></h2>
<div class="tabbed-set tabbed-alternate" data-tabs="1:2"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Minimal (Core Only)</label><label for="__tabbed_1_2">Full Stack</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
<p>For a basic deployment with campaigns, map, and admin:</p>
<div class="language-bash highlight"><span class="filename">Required variables</span><pre><span></span><code><span id="__span-5-1"><a id="__codelineno-5-1" name="__codelineno-5-1" href="#__codelineno-5-1"></a><span class="nv">V2_POSTGRES_PASSWORD</span><span class="o">=</span>...
</span><span id="__span-5-2"><a id="__codelineno-5-2" name="__codelineno-5-2" href="#__codelineno-5-2"></a><span class="nv">REDIS_PASSWORD</span><span class="o">=</span>...
</span><span id="__span-5-3"><a id="__codelineno-5-3" name="__codelineno-5-3" href="#__codelineno-5-3"></a><span class="nv">JWT_ACCESS_SECRET</span><span class="o">=</span>...
</span><span id="__span-5-4"><a id="__codelineno-5-4" name="__codelineno-5-4" href="#__codelineno-5-4"></a><span class="nv">JWT_REFRESH_SECRET</span><span class="o">=</span>...
</span><span id="__span-5-5"><a id="__codelineno-5-5" name="__codelineno-5-5" href="#__codelineno-5-5"></a><span class="nv">JWT_INVITE_SECRET</span><span class="o">=</span>...
</span><span id="__span-5-6"><a id="__codelineno-5-6" name="__codelineno-5-6" href="#__codelineno-5-6"></a><span class="nv">ENCRYPTION_KEY</span><span class="o">=</span>...
</span><span id="__span-5-7"><a id="__codelineno-5-7" name="__codelineno-5-7" href="#__codelineno-5-7"></a><span class="nv">INITIAL_ADMIN_PASSWORD</span><span class="o">=</span>...
</span></code></pre></div>
<div class="language-bash highlight"><span class="filename">Start services</span><pre><span></span><code><span id="__span-6-1"><a id="__codelineno-6-1" name="__codelineno-6-1" href="#__codelineno-6-1"></a>docker<span class="w"> </span>compose<span class="w"> </span>up<span class="w"> </span>-d<span class="w"> </span>v2-postgres<span class="w"> </span>redis<span class="w"> </span>api<span class="w"> </span>admin
</span></code></pre></div>
</div>
<div class="tabbed-block">
<p>For the complete platform including media, newsletters, monitoring, and all services:</p>
<div class="language-bash highlight"><span class="filename">Additional variables needed</span><pre><span></span><code><span id="__span-7-1"><a id="__codelineno-7-1" name="__codelineno-7-1" href="#__codelineno-7-1"></a><span class="c1"># Everything above, plus:</span>
</span><span id="__span-7-2"><a id="__codelineno-7-2" name="__codelineno-7-2" href="#__codelineno-7-2"></a><span class="nv">ENABLE_MEDIA_FEATURES</span><span class="o">=</span><span class="nb">true</span>
</span><span id="__span-7-3"><a id="__codelineno-7-3" name="__codelineno-7-3" href="#__codelineno-7-3"></a><span class="nv">ENABLE_PAYMENTS</span><span class="o">=</span><span class="nb">true</span>
</span><span id="__span-7-4"><a id="__codelineno-7-4" name="__codelineno-7-4" href="#__codelineno-7-4"></a><span class="nv">ENABLE_CHAT</span><span class="o">=</span><span class="nb">true</span>
</span><span id="__span-7-5"><a id="__codelineno-7-5" name="__codelineno-7-5" href="#__codelineno-7-5"></a><span class="nv">ENABLE_MEET</span><span class="o">=</span><span class="nb">true</span>
</span><span id="__span-7-6"><a id="__codelineno-7-6" name="__codelineno-7-6" href="#__codelineno-7-6"></a><span class="nv">ENABLE_SMS</span><span class="o">=</span><span class="nb">true</span>
</span><span id="__span-7-7"><a id="__codelineno-7-7" name="__codelineno-7-7" href="#__codelineno-7-7"></a><span class="nv">LISTMONK_SYNC_ENABLED</span><span class="o">=</span><span class="nb">true</span>
</span><span id="__span-7-8"><a id="__codelineno-7-8" name="__codelineno-7-8" href="#__codelineno-7-8"></a><span class="nv">GANCIO_SYNC_ENABLED</span><span class="o">=</span><span class="nb">true</span>
</span><span id="__span-7-9"><a id="__codelineno-7-9" name="__codelineno-7-9" href="#__codelineno-7-9"></a><span class="nv">LISTMONK_DB_PASSWORD</span><span class="o">=</span>...
</span><span id="__span-7-10"><a id="__codelineno-7-10" name="__codelineno-7-10" href="#__codelineno-7-10"></a><span class="nv">LISTMONK_WEB_ADMIN_PASSWORD</span><span class="o">=</span>...
</span><span id="__span-7-11"><a id="__codelineno-7-11" name="__codelineno-7-11" href="#__codelineno-7-11"></a><span class="nv">LISTMONK_API_TOKEN</span><span class="o">=</span>...
</span><span id="__span-7-12"><a id="__codelineno-7-12" name="__codelineno-7-12" href="#__codelineno-7-12"></a><span class="nv">NC_ADMIN_PASSWORD</span><span class="o">=</span>...
</span><span id="__span-7-13"><a id="__codelineno-7-13" name="__codelineno-7-13" href="#__codelineno-7-13"></a><span class="nv">GITEA_DB_PASSWD</span><span class="o">=</span>...
</span><span id="__span-7-14"><a id="__codelineno-7-14" name="__codelineno-7-14" href="#__codelineno-7-14"></a><span class="nv">GITEA_DB_ROOT_PASSWORD</span><span class="o">=</span>...
</span><span id="__span-7-15"><a id="__codelineno-7-15" name="__codelineno-7-15" href="#__codelineno-7-15"></a><span class="nv">N8N_ENCRYPTION_KEY</span><span class="o">=</span>...
</span><span id="__span-7-16"><a id="__codelineno-7-16" name="__codelineno-7-16" href="#__codelineno-7-16"></a><span class="nv">N8N_USER_PASSWORD</span><span class="o">=</span>...
</span><span id="__span-7-17"><a id="__codelineno-7-17" name="__codelineno-7-17" href="#__codelineno-7-17"></a><span class="nv">VAULTWARDEN_ADMIN_TOKEN</span><span class="o">=</span>...
</span><span id="__span-7-18"><a id="__codelineno-7-18" name="__codelineno-7-18" href="#__codelineno-7-18"></a><span class="nv">ROCKETCHAT_ADMIN_PASSWORD</span><span class="o">=</span>...
</span><span id="__span-7-19"><a id="__codelineno-7-19" name="__codelineno-7-19" href="#__codelineno-7-19"></a><span class="nv">MONGO_ROOT_PASSWORD</span><span class="o">=</span>...
</span><span id="__span-7-20"><a id="__codelineno-7-20" name="__codelineno-7-20" href="#__codelineno-7-20"></a><span class="nv">GANCIO_ADMIN_PASSWORD</span><span class="o">=</span>...
</span><span id="__span-7-21"><a id="__codelineno-7-21" name="__codelineno-7-21" href="#__codelineno-7-21"></a><span class="nv">JITSI_APP_SECRET</span><span class="o">=</span>...
</span><span id="__span-7-22"><a id="__codelineno-7-22" name="__codelineno-7-22" href="#__codelineno-7-22"></a><span class="nv">JITSI_JICOFO_AUTH_PASSWORD</span><span class="o">=</span>...
</span><span id="__span-7-23"><a id="__codelineno-7-23" name="__codelineno-7-23" href="#__codelineno-7-23"></a><span class="nv">JITSI_JVB_AUTH_PASSWORD</span><span class="o">=</span>...
</span><span id="__span-7-24"><a id="__codelineno-7-24" name="__codelineno-7-24" href="#__codelineno-7-24"></a><span class="nv">JVB_ADVERTISE_IP</span><span class="o">=</span>your.public.ip.here
</span><span id="__span-7-25"><a id="__codelineno-7-25" name="__codelineno-7-25" href="#__codelineno-7-25"></a><span class="nv">EMAIL_TEST_MODE</span><span class="o">=</span><span class="nb">false</span>
</span><span id="__span-7-26"><a id="__codelineno-7-26" name="__codelineno-7-26" href="#__codelineno-7-26"></a><span class="nv">SMTP_HOST</span><span class="o">=</span>smtp.your-provider.com
</span><span id="__span-7-27"><a id="__codelineno-7-27" name="__codelineno-7-27" href="#__codelineno-7-27"></a><span class="nv">SMTP_PORT</span><span class="o">=</span><span class="m">587</span>
</span><span id="__span-7-28"><a id="__codelineno-7-28" name="__codelineno-7-28" href="#__codelineno-7-28"></a><span class="nv">SMTP_USER</span><span class="o">=</span>you@example.com
</span><span id="__span-7-29"><a id="__codelineno-7-29" name="__codelineno-7-29" href="#__codelineno-7-29"></a><span class="nv">SMTP_PASS</span><span class="o">=</span>your-smtp-password
</span></code></pre></div>
<div class="language-bash highlight"><span class="filename">Start services</span><pre><span></span><code><span id="__span-8-1"><a id="__codelineno-8-1" name="__codelineno-8-1" href="#__codelineno-8-1"></a>docker<span class="w"> </span>compose<span class="w"> </span>up<span class="w"> </span>-d
</span><span id="__span-8-2"><a id="__codelineno-8-2" name="__codelineno-8-2" href="#__codelineno-8-2"></a>docker<span class="w"> </span>compose<span class="w"> </span>--profile<span class="w"> </span>monitoring<span class="w"> </span>up<span class="w"> </span>-d
</span></code></pre></div>
</div>
</div>
</div>
</article>
</div>
<script>var tabs=__md_get("__tabs");if(Array.isArray(tabs))e:for(var set of document.querySelectorAll(".tabbed-set")){var labels=set.querySelector(".tabbed-labels");for(var tab of tabs)for(var label of labels.getElementsByTagName("label"))if(label.innerText.trim()===tab){var input=document.getElementById(label.htmlFor);input.checked=!0;continue e}}</script>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg>
Back to top
</button>
</main>
<footer class="md-footer">
<nav class="md-footer__inner md-grid" aria-label="Footer" >
<a href="../services/" class="md-footer__link md-footer__link--prev" aria-label="Previous: Services Overview">
<div class="md-footer__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</div>
<div class="md-footer__title">
<span class="md-footer__direction">
Previous
</span>
<div class="md-ellipsis">
Services Overview
</div>
</div>
</a>
<a href="../first-steps/" class="md-footer__link md-footer__link--next" aria-label="Next: First Steps">
<div class="md-footer__title">
<span class="md-footer__direction">
Next
</span>
<div class="md-ellipsis">
First Steps
</div>
</div>
<div class="md-footer__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 11v2h12l-5.5 5.5 1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5 16 11z"/></svg>
</div>
</a>
</nav>
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
<div class="md-copyright__highlight">
Copyright &copy; 20242026 The Bunker Operations <a href="#__consent">Change cookie settings</a>
</div>
Made with
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
Material for MkDocs
</a>
</div>
<div class="md-social">
<a href="https://gitea.bnkops.com/admin" target="_blank" rel="noopener" title="Gitea Repository" class="md-social__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M80 104a24 24 0 1 0 0-48 24 24 0 1 0 0 48m80-24c0 32.8-19.7 61-48 73.3V224h176c26.5 0 48-21.5 48-48v-22.7c-28.3-12.3-48-40.5-48-73.3 0-44.2 35.8-80 80-80s80 35.8 80 80c0 32.8-19.7 61-48 73.3V176c0 61.9-50.1 112-112 112H112v70.7c28.3 12.3 48 40.5 48 73.3 0 44.2-35.8 80-80 80S0 476.2 0 432c0-32.8 19.7-61 48-73.3V153.4C19.7 141 0 112.8 0 80 0 35.8 35.8 0 80 0s80 35.8 80 80m232 0a24 24 0 1 0-48 0 24 24 0 1 0 48 0M80 456a24 24 0 1 0 0-48 24 24 0 1 0 0 48"/></svg>
</a>
<a href="https://listmonk.bnkops.com/subscription/form" target="_blank" rel="noopener" title="Newsletter" class="md-social__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M536.4-26.3c9.8-3.5 20.6-1 28 6.3s9.8 18.2 6.3 28l-178 496.9c-5 13.9-18.1 23.1-32.8 23.1-14.2 0-27-8.6-32.3-21.7l-64.2-158c-4.5-11-2.5-23.6 5.2-32.6l94.5-112.4c5.1-6.1 4.7-15-.9-20.6s-14.6-6-20.6-.9l-112.4 94.3c-9.1 7.6-21.6 9.6-32.6 5.2L38.1 216.8c-13.1-5.3-21.7-18.1-21.7-32.3 0-14.7 9.2-27.8 23.1-32.8z"/></svg>
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<div class="md-progress" data-md-component="progress" role="progressbar"></div>
<script id="__config" type="application/json">{"annotate": null, "base": "../../..", "features": ["announce.dismiss", "content.action.edit", "content.action.view", "content.code.annotate", "content.code.copy", "content.code.select", "content.tabs.link", "content.tooltips", "navigation.footer", "navigation.indexes", "navigation.instant", "navigation.instant.prefetch", "navigation.instant.progress", "navigation.path", "navigation.prune", "navigation.tabs", "navigation.tabs.sticky", "navigation.top", "navigation.tracking", "search.highlight", "search.share", "search.suggest", "toc.follow"], "search": "../../../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="../../../assets/javascripts/bundle.79ae519e.min.js"></script>
<script src="../../../javascripts/home.js"></script>
<script src="../../../javascripts/github-widget.js"></script>
<script src="../../../javascripts/gitea-widget.js"></script>
<script src="../../../assets/js/env-config.js"></script>
<script src="../../../assets/js/video-player.js"></script>
<script src="../../../assets/js/image-gallery.js"></script>
<script src="../../../assets/js/gancio-events.js"></script>
<script src="../../../assets/js/payment-widgets.js"></script>
<script src="../../../assets/js/scheduling-poll.js"></script>
<script src="../../../assets/js/straw-poll-widget.js"></script>
<script src="../../../javascripts/ad-widgets.js"></script>
<script src="../../../javascripts/docs-comments.js"></script>
</body>
</html>