changemaker.lite/config.sh
bunker-admin 82a66a97d0 Add MONGO_ROOT_PASSWORD to docs, config wizard, CCP, and prod compose
Follow-up to security audit commit — propagates MongoDB auth
(--auth flag) across all deployment paths:

- mkdocs environment-variables.md: add MONGO_ROOT_PASSWORD + MONGO_ROOT_USER,
  update ENCRYPTION_KEY description (now required in all environments),
  add to secret generation and full-stack variable lists
- config.sh: generate MONGO_ROOT_PASSWORD alongside Rocket.Chat credentials
- docker-compose.prod.yml: add --auth + credentials to MongoDB, update
  Rocket.Chat MONGO_URL with auth params
- CCP env.hbs: add MONGO_ROOT_USER/PASSWORD to chat block
- CCP docker-compose.yml.hbs: same MongoDB auth + MONGO_URL changes
- CCP secret-generator.ts: add mongoRootPassword to InstanceSecrets

Bunker Admin
2026-03-27 08:57:48 -06:00

1425 lines
46 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# =============================================================================
# Changemaker Lite V2 — Configuration Wizard
# Produces a working .env file and stages the full application stack.
# =============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="$SCRIPT_DIR/.env"
ENV_EXAMPLE="$SCRIPT_DIR/.env.example"
MKDOCS_YML="$SCRIPT_DIR/mkdocs/mkdocs.yml"
SERVICES_YAML="$SCRIPT_DIR/configs/homepage/services.yaml"
# --- Detect install mode ---
# Release mode: installed from tarball (has VERSION file, no .git directory)
# Source mode: cloned from git repository
if [[ -f "$SCRIPT_DIR/VERSION" ]] && [[ ! -d "$SCRIPT_DIR/.git" ]]; then
INSTALL_MODE="release"
RELEASE_VERSION=$(head -1 "$SCRIPT_DIR/VERSION")
else
INSTALL_MODE="source"
RELEASE_VERSION=""
fi
# --- Colors (respects NO_COLOR convention) ---
if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then
RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m'
BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m'
DIM='\033[2m' NC='\033[0m'
else
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' DIM='' NC=''
fi
# =============================================================================
# Ensure stdin is connected to the terminal (handles curl | bash case)
# =============================================================================
if [[ ! -t 0 ]]; then
if [[ -e /dev/tty ]]; then
exec 0</dev/tty
else
echo "[ERR] This script requires an interactive terminal." >&2
echo " Download and run manually: bash config.sh" >&2
exit 1
fi
fi
# =============================================================================
# Utility Functions
# =============================================================================
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
success() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERR]${NC} $*" >&2; }
header() {
echo ""
echo -e "${BOLD}${BLUE}── $* ──${NC}"
echo ""
}
generate_secret() {
openssl rand -hex 32
}
generate_password() {
local length=${1:-24}
openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c "$length"
}
# Line-by-line .env replacement — robust with special characters
update_env_var() {
local key=$1
local value=$2
if grep -q "^${key}=" "$ENV_FILE"; then
local tmpfile
tmpfile=$(mktemp)
while IFS= read -r line; do
if [[ "$line" =~ ^${key}= ]]; then
echo "${key}=${value}" >> "$tmpfile"
else
echo "$line" >> "$tmpfile"
fi
done < "$ENV_FILE"
mv "$tmpfile" "$ENV_FILE"
else
echo "${key}=${value}" >> "$ENV_FILE"
fi
}
backup_env_file() {
if [[ -f "$ENV_FILE" ]]; then
local ts
ts=$(date +"%Y%m%d_%H%M%S")
local backup="$ENV_FILE.backup_$ts"
cp "$ENV_FILE" "$backup"
success "Backed up .env to ${backup##*/}"
fi
}
validate_password() {
local pw=$1
[[ ${#pw} -ge 12 ]] || return 1
[[ "$pw" =~ [A-Z] ]] || return 1
[[ "$pw" =~ [a-z] ]] || return 1
[[ "$pw" =~ [0-9] ]] || return 1
return 0
}
prompt_yes_no() {
local prompt=$1 default=${2:-n}
local yn
if [[ "$default" == "y" ]]; then
read -rp "$prompt [Y/n]: " yn
[[ "$yn" =~ ^[Nn]$ ]] && return 1 || return 0
else
read -rp "$prompt [y/N]: " yn
[[ "$yn" =~ ^[Yy]$ ]] && return 0 || return 1
fi
}
# =============================================================================
# Prerequisites
# =============================================================================
check_prerequisites() {
header "Checking Prerequisites"
local ok=true
if command -v docker &>/dev/null; then
success "Docker found: $(docker --version | head -1)"
else
error "Docker is not installed. See https://docs.docker.com/get-docker/"
ok=false
fi
if docker compose version &>/dev/null; then
success "Docker Compose found: $(docker compose version --short)"
else
error "Docker Compose v2 plugin not found. See https://docs.docker.com/compose/install/"
ok=false
fi
if command -v openssl &>/dev/null; then
success "OpenSSL found"
else
error "OpenSSL is not installed (needed for secret generation)"
ok=false
fi
$ok || { echo ""; error "Missing prerequisites. Install them and re-run."; exit 1; }
}
# =============================================================================
# Banner
# =============================================================================
print_banner() {
cat << 'EOF'
██████╗██╗ ██╗ █████╗ ███╗ ██╗ ██████╗ ███████╗
██╔════╝██║ ██║██╔══██╗████╗ ██║██╔════╝ ██╔════╝
██║ ███████║███████║██╔██╗ ██║██║ ███╗█████╗
██║ ██╔══██║██╔══██║██║╚██╗██║██║ ██║██╔══╝
╚██████╗██║ ██║██║ ██║██║ ╚████║╚██████╔╝███████╗
╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝
███╗ ███╗ █████╗ ██╗ ██╗███████╗██████╗
████╗ ████║██╔══██╗██║ ██╔╝██╔════╝██╔══██╗
██╔████╔██║███████║█████╔╝ █████╗ ██████╔╝
██║╚██╔╝██║██╔══██║██╔═██╗ ██╔══╝ ██╔══██╗
██║ ╚═╝ ██║██║ ██║██║ ██╗███████╗██║ ██║
╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝
V2 Configuration Wizard
EOF
echo ""
info "This wizard will create your .env file, generate secure secrets,"
info "and prepare your system to run the full Changemaker Lite stack."
echo ""
}
# =============================================================================
# .env Initialization
# =============================================================================
initialize_env() {
header "Environment File Setup"
if [[ ! -f "$ENV_EXAMPLE" ]]; then
error ".env.example not found at $ENV_EXAMPLE"
error "Make sure you're running this from the project root."
exit 1
fi
RECONFIGURE_MODE=false
if [[ -f "$ENV_FILE" ]]; then
warn "Existing .env file found at $ENV_FILE"
if prompt_yes_no "Back up existing .env and create a fresh one?"; then
backup_env_file
cp "$ENV_EXAMPLE" "$ENV_FILE"
success "Created fresh .env from .env.example"
else
info "Keeping existing .env. Existing secrets will be preserved."
RECONFIGURE_MODE=true
fi
else
cp "$ENV_EXAMPLE" "$ENV_FILE"
success "Created .env from .env.example"
fi
}
# =============================================================================
# Configuration Sections
# =============================================================================
configure_domain() {
header "Domain Configuration"
info "Root domain serves MkDocs documentation."
info "All application routes are at app.<domain>."
echo ""
read -rp "Enter your domain (e.g., example.org) [default: cmlite.org]: " domain
domain=${domain:-cmlite.org}
update_env_var "DOMAIN" "$domain"
update_env_var "BASE_DOMAIN" "https://$domain"
update_env_var "GITEA_ROOT_URL" "https://git.$domain"
update_env_var "GITEA_DOMAIN" "git.$domain"
update_env_var "N8N_HOST" "n8n.$domain"
update_env_var "SMTP_FROM" "noreply@$domain"
update_env_var "SMTP_FROM_NAME" "Changemaker Lite"
update_env_var "INITIAL_ADMIN_EMAIL" "admin@$domain"
update_env_var "NC_ADMIN_EMAIL" "admin@$domain"
update_env_var "TEST_EMAIL_RECIPIENT" "admin@$domain"
update_env_var "EXCALIDRAW_WS_URL" "wss://draw.$domain"
update_env_var "LISTMONK_SMTP_FROM" "Changemaker Lite <noreply@$domain>"
update_env_var "HOMEPAGE_VAR_BASE_URL" "https://$domain"
update_env_var "VAULTWARDEN_DOMAIN" "https://vault.$domain"
update_env_var "GANCIO_BASE_URL" "https://events.$domain"
# Update mkdocs.yml
if [[ -f "$MKDOCS_YML" ]]; then
sed -i "s|^site_url:.*|site_url: https://$domain|" "$MKDOCS_YML"
sed -i "s|^repo_url:.*|repo_url: https://git.$domain/admin/changemaker.lite|" "$MKDOCS_YML"
success "Updated mkdocs.yml (site_url, repo_url)"
else
warn "mkdocs.yml not found — skipping"
fi
success "Domain set to: $domain"
echo ""
if prompt_yes_no "Is this a production deployment?"; then
update_env_var "NODE_ENV" "production"
success "NODE_ENV set to production"
IS_PRODUCTION="yes"
else
info "NODE_ENV stays as development"
IS_PRODUCTION="no"
fi
# Store for later use
CONFIGURED_DOMAIN="$domain"
}
configure_admin() {
header "Admin Credentials"
local default_email="admin@${CONFIGURED_DOMAIN:-cmlite.org}"
read -rp "Admin email [default: $default_email]: " admin_email
admin_email=${admin_email:-$default_email}
local admin_password=""
while true; do
echo ""
read -rsp "Admin password (min 12 chars, uppercase + lowercase + digit): " admin_password
echo ""
if validate_password "$admin_password"; then
read -rsp "Confirm password: " confirm_password
echo ""
if [[ "$admin_password" == "$confirm_password" ]]; then
break
else
warn "Passwords do not match. Try again."
fi
else
warn "Password must be 12+ characters with uppercase, lowercase, and a digit."
fi
done
update_env_var "INITIAL_ADMIN_EMAIL" "$admin_email"
update_env_var "INITIAL_ADMIN_PASSWORD" "$admin_password"
update_env_var "N8N_USER_EMAIL" "$admin_email"
success "Admin credentials configured"
}
# Check if a key already has a real value in .env (non-empty, not a placeholder)
env_var_is_set() {
local key=$1
local val
val=$(grep "^${key}=" "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2-)
[[ -n "$val" && "$val" != "changeme" && "$val" != *"example"* && "$val" != *"CHANGEME"* ]]
}
# Update an env var only if not already set (for reconfigure mode)
update_env_var_if_empty() {
local key=$1
local value=$2
if [[ "$RECONFIGURE_MODE" == "true" ]] && env_var_is_set "$key"; then
return 1 # signal: kept existing
fi
update_env_var "$key" "$value"
return 0
}
generate_all_secrets() {
header "Generating Secrets"
if [[ "$RECONFIGURE_MODE" == "true" ]]; then
info "Reconfigure mode: existing secrets will be preserved."
else
info "Auto-generating 22 unique secrets and passwords..."
fi
echo ""
local generated=0 kept=0
# JWT & Encryption (64-char hex)
local jwt_access jwt_refresh jwt_invite enc_key
jwt_access=$(generate_secret)
jwt_refresh=$(generate_secret)
jwt_invite=$(generate_secret)
enc_key=$(generate_secret)
local jwt_changed=false
update_env_var_if_empty "JWT_ACCESS_SECRET" "$jwt_access" && jwt_changed=true
update_env_var_if_empty "JWT_REFRESH_SECRET" "$jwt_refresh" && jwt_changed=true
update_env_var_if_empty "JWT_INVITE_SECRET" "$jwt_invite" && jwt_changed=true
update_env_var_if_empty "ENCRYPTION_KEY" "$enc_key" && jwt_changed=true
if [[ "$jwt_changed" == "true" ]]; then
success "JWT secrets + encryption key"
((generated+=4))
else
info "JWT secrets + encryption key (kept existing)"
((kept+=4))
fi
# Database passwords (24-char alphanum)
local pg_pass redis_pass
pg_pass=$(generate_password 24)
redis_pass=$(generate_password 24)
local db_changed=false
if update_env_var_if_empty "V2_POSTGRES_PASSWORD" "$pg_pass"; then
update_env_var "DATABASE_URL" "postgresql://changemaker:${pg_pass}@localhost:5433/changemaker_v2"
db_changed=true
fi
update_env_var_if_empty "REDIS_PASSWORD" "$redis_pass" && db_changed=true
if [[ "$db_changed" == "true" ]]; then
# Rebuild REDIS_URL if password changed
local current_redis_pass
current_redis_pass=$(grep "^REDIS_PASSWORD=" "$ENV_FILE" | cut -d= -f2-)
update_env_var "REDIS_URL" "redis://:${current_redis_pass}@redis-changemaker:6379"
success "PostgreSQL + Redis passwords"
((generated+=2))
else
info "PostgreSQL + Redis passwords (kept existing)"
((kept+=2))
fi
# Listmonk
local lm_db_pass lm_web_pass lm_api_token
lm_db_pass=$(generate_password 24)
lm_web_pass=$(generate_password 20)
lm_api_token=$(openssl rand -hex 16)
local lm_changed=false
update_env_var_if_empty "LISTMONK_DB_PASSWORD" "$lm_db_pass" && lm_changed=true
update_env_var_if_empty "LISTMONK_WEB_ADMIN_PASSWORD" "$lm_web_pass" && lm_changed=true
if update_env_var_if_empty "LISTMONK_API_TOKEN" "$lm_api_token"; then
update_env_var "LISTMONK_ADMIN_PASSWORD" "$lm_api_token"
lm_changed=true
fi
if [[ "$lm_changed" == "true" ]]; then
success "Listmonk passwords + API token"
((generated+=3))
else
info "Listmonk passwords + API token (kept existing)"
((kept+=3))
fi
# NocoDB
local nc_pass
nc_pass=$(generate_password 20)
if update_env_var_if_empty "NC_ADMIN_PASSWORD" "$nc_pass"; then
success "NocoDB admin password"
((generated++))
else
info "NocoDB admin password (kept existing)"
((kept++))
fi
# Gitea
local gitea_db gitea_root
gitea_db=$(generate_password 20)
gitea_root=$(generate_password 20)
local gitea_changed=false
update_env_var_if_empty "GITEA_DB_PASSWD" "$gitea_db" && gitea_changed=true
update_env_var_if_empty "GITEA_DB_ROOT_PASSWORD" "$gitea_root" && gitea_changed=true
if [[ "$gitea_changed" == "true" ]]; then
success "Gitea database passwords"
((generated+=2))
else
info "Gitea database passwords (kept existing)"
((kept+=2))
fi
# n8n
local n8n_enc n8n_pass
n8n_enc=$(generate_password 32)
n8n_pass=$(generate_password 20)
local n8n_changed=false
update_env_var_if_empty "N8N_ENCRYPTION_KEY" "$n8n_enc" && n8n_changed=true
update_env_var_if_empty "N8N_USER_PASSWORD" "$n8n_pass" && n8n_changed=true
if [[ "$n8n_changed" == "true" ]]; then
success "n8n encryption key + admin password"
((generated+=2))
else
info "n8n encryption key + admin password (kept existing)"
((kept+=2))
fi
# Monitoring
local grafana_pass gotify_pass
grafana_pass=$(generate_password 20)
gotify_pass=$(generate_password 20)
local mon_changed=false
update_env_var_if_empty "GRAFANA_ADMIN_PASSWORD" "$grafana_pass" && mon_changed=true
update_env_var_if_empty "GOTIFY_ADMIN_PASSWORD" "$gotify_pass" && mon_changed=true
if [[ "$mon_changed" == "true" ]]; then
success "Grafana + Gotify admin passwords"
((generated+=2))
else
info "Grafana + Gotify admin passwords (kept existing)"
((kept+=2))
fi
# Vaultwarden
local vw_admin_token
vw_admin_token=$(generate_secret)
if update_env_var_if_empty "VAULTWARDEN_ADMIN_TOKEN" "$vw_admin_token"; then
success "Vaultwarden admin token"
((generated++))
else
info "Vaultwarden admin token (kept existing)"
((kept++))
fi
# Rocket.Chat
local rc_pass
rc_pass=$(generate_password 20)
if update_env_var_if_empty "ROCKETCHAT_ADMIN_PASSWORD" "$rc_pass"; then
success "Rocket.Chat admin password"
((generated++))
else
info "Rocket.Chat admin password (kept existing)"
((kept++))
fi
# MongoDB (required for Rocket.Chat — runs with --auth)
local mongo_pass
mongo_pass=$(generate_password 24)
if update_env_var_if_empty "MONGO_ROOT_PASSWORD" "$mongo_pass"; then
success "MongoDB root password"
((generated++))
else
info "MongoDB root password (kept existing)"
((kept++))
fi
# Gancio
local gancio_pass
gancio_pass=$(generate_password 20)
if update_env_var_if_empty "GANCIO_ADMIN_PASSWORD" "$gancio_pass"; then
success "Gancio admin password"
((generated++))
else
info "Gancio admin password (kept existing)"
((kept++))
fi
# Jitsi Meet
local jitsi_secret jitsi_jicofo jitsi_jvb
jitsi_secret=$(generate_secret)
jitsi_jicofo=$(openssl rand -hex 16)
jitsi_jvb=$(openssl rand -hex 16)
local jitsi_changed=false
update_env_var_if_empty "JITSI_APP_SECRET" "$jitsi_secret" && jitsi_changed=true
update_env_var_if_empty "JITSI_JICOFO_AUTH_PASSWORD" "$jitsi_jicofo" && jitsi_changed=true
update_env_var_if_empty "JITSI_JVB_AUTH_PASSWORD" "$jitsi_jvb" && jitsi_changed=true
if [[ "$jitsi_changed" == "true" ]]; then
success "Jitsi Meet secrets (JWT + XMPP)"
((generated+=3))
else
info "Jitsi Meet secrets (kept existing)"
((kept+=3))
fi
echo ""
if [[ $kept -gt 0 ]]; then
success "Secrets: ${generated} generated, ${kept} preserved from existing .env"
else
success "All 22 secrets generated. No placeholder passwords remain."
fi
}
configure_smtp() {
header "Email Configuration"
info "By default, emails are captured by MailHog (test mode)."
info "You can configure a production SMTP server now or later."
echo ""
if prompt_yes_no "Configure production SMTP now?"; then
read -rp " SMTP host (e.g., smtp.protonmail.ch): " smtp_host
read -rp " SMTP port (e.g., 587): " smtp_port
read -rp " SMTP user: " smtp_user
read -rsp " SMTP password: " smtp_pass
echo ""
update_env_var "SMTP_HOST" "$smtp_host"
update_env_var "SMTP_PORT" "$smtp_port"
update_env_var "SMTP_USER" "$smtp_user"
update_env_var "SMTP_PASS" "$smtp_pass"
update_env_var "EMAIL_TEST_MODE" "false"
if prompt_yes_no " Also use this SMTP for Listmonk newsletters?"; then
update_env_var "LISTMONK_SMTP_HOST" "$smtp_host"
update_env_var "LISTMONK_SMTP_PORT" "$smtp_port"
update_env_var "LISTMONK_SMTP_USER" "$smtp_user"
update_env_var "LISTMONK_SMTP_PASSWORD" "$smtp_pass"
update_env_var "LISTMONK_SMTP_TLS_TYPE" "STARTTLS"
success "Listmonk SMTP configured"
fi
# Vaultwarden needs matching SMTP security
update_env_var "VAULTWARDEN_SMTP_SECURITY" "starttls"
success "Production SMTP configured"
SMTP_MODE="production"
else
info "Using MailHog for email testing (port 8025)"
update_env_var "VAULTWARDEN_SMTP_SECURITY" "off"
SMTP_MODE="mailhog"
fi
}
configure_features() {
header "Feature Flags"
if prompt_yes_no "Enable Media Manager (video library)?"; then
update_env_var "ENABLE_MEDIA_FEATURES" "true"
success "Media Manager enabled"
MEDIA_ENABLED="yes"
else
MEDIA_ENABLED="no"
fi
if prompt_yes_no "Enable Listmonk newsletter sync?"; then
update_env_var "LISTMONK_SYNC_ENABLED" "true"
success "Listmonk sync enabled"
LISTMONK_SYNC="yes"
else
LISTMONK_SYNC="no"
fi
if prompt_yes_no "Enable Payments (Stripe)?"; then
update_env_var "ENABLE_PAYMENTS" "true"
success "Payments enabled"
PAYMENTS_ENABLED="yes"
else
PAYMENTS_ENABLED="no"
fi
if prompt_yes_no "Enable Rocket.Chat (team chat)?"; then
update_env_var "ENABLE_CHAT" "true"
success "Rocket.Chat enabled"
CHAT_ENABLED="yes"
else
CHAT_ENABLED="no"
fi
if prompt_yes_no "Enable Gancio event sync (shift → event)?"; then
update_env_var "GANCIO_SYNC_ENABLED" "true"
success "Gancio sync enabled"
GANCIO_SYNC="yes"
else
GANCIO_SYNC="no"
fi
if prompt_yes_no "Enable Jitsi Meet (video conferencing)?"; then
update_env_var "ENABLE_MEET" "true"
success "Jitsi Meet enabled"
MEET_ENABLED="yes"
echo ""
info "Jitsi requires your server's public IP for media traffic (NAT traversal)."
info "Firewall must allow UDP port 10000 for video/audio."
read -rp " Server public IP [leave blank to set later]: " jvb_ip
if [[ -n "$jvb_ip" ]]; then
update_env_var "JVB_ADVERTISE_IP" "$jvb_ip"
success "JVB advertise IP set to $jvb_ip"
else
warn "Set JVB_ADVERTISE_IP in .env before starting Jitsi containers."
fi
else
MEET_ENABLED="no"
fi
if prompt_yes_no "Enable SMS Campaigns (Termux Android bridge)?"; then
update_env_var "ENABLE_SMS" "true"
success "SMS Campaigns enabled"
SMS_ENABLED="yes"
echo ""
info "SMS uses a Termux-based Android phone as the sending device."
read -rp " Termux API URL [default: http://10.0.0.193:5001]: " termux_url
termux_url=${termux_url:-http://10.0.0.193:5001}
update_env_var "TERMUX_API_URL" "$termux_url"
read -rp " Termux API Key [leave blank to set later]: " termux_key
if [[ -n "$termux_key" ]]; then
update_env_var "TERMUX_API_KEY" "$termux_key"
fi
else
SMS_ENABLED="no"
fi
if prompt_yes_no "Enable Docs Comments (Gitea-backed page comments)?"; then
update_env_var "GITEA_COMMENTS_ENABLED" "true"
success "Docs Comments enabled"
DOCS_COMMENTS_ENABLED="yes"
info "After Gitea is running, create a Personal Access Token and OAuth2 app,"
info "then set GITEA_API_TOKEN, GITEA_OAUTH_CLIENT_ID/SECRET in .env."
else
DOCS_COMMENTS_ENABLED="no"
fi
if prompt_yes_no "Enable Monitoring stack (Prometheus, Grafana, Alertmanager, cAdvisor)?" "y"; then
update_env_var "COMPOSE_PROFILES" "monitoring"
success "Monitoring enabled (COMPOSE_PROFILES=monitoring)"
MONITORING_ENABLED="yes"
else
MONITORING_ENABLED="no"
fi
if prompt_yes_no "Enable Bunker Ops (fleet metrics push to central server)?"; then
update_env_var "BUNKER_OPS_ENABLED" "true"
success "Bunker Ops enabled"
BUNKER_OPS_ENABLED="yes"
echo ""
read -rp " Instance label [default: domain name]: " instance_label
if [[ -n "$instance_label" ]]; then
update_env_var "INSTANCE_LABEL" "$instance_label"
fi
read -rp " Remote write URL (VictoriaMetrics endpoint): " remote_write_url
if [[ -n "$remote_write_url" ]]; then
update_env_var "BUNKER_OPS_REMOTE_WRITE_URL" "$remote_write_url"
fi
else
BUNKER_OPS_ENABLED="no"
fi
}
configure_pangolin() {
header "Tunnel Configuration (Pangolin)"
info "Pangolin provides secure public access to your services."
info "Skip this if you'll configure tunneling later."
echo ""
if prompt_yes_no "Configure Pangolin tunnel now?"; then
read -rp " Pangolin API URL [default: https://api.bnkserve.org/v1]: " pang_url
pang_url=${pang_url:-https://api.bnkserve.org/v1}
read -rp " Pangolin API key: " pang_key
read -rp " Pangolin Organization ID: " pang_org
update_env_var "PANGOLIN_API_URL" "$pang_url"
update_env_var "PANGOLIN_API_KEY" "$pang_key"
update_env_var "PANGOLIN_ORG_ID" "$pang_org"
success "Pangolin configured"
info "Complete tunnel setup in the admin GUI at /app/pangolin after starting services."
PANGOLIN_CONFIGURED="yes"
else
PANGOLIN_CONFIGURED="no"
fi
}
configure_cors() {
local domain="${CONFIGURED_DOMAIN:-cmlite.org}"
# Include app subdomain + root domain (for MkDocs payment widgets) + localhost fallbacks
local origins="http://app.$domain,https://app.$domain,http://$domain,https://$domain,http://localhost:3000,http://localhost,http://localhost:4003"
update_env_var "CORS_ORIGINS" "$origins"
success "CORS origins set for $domain"
}
generate_nginx_configs() {
header "Nginx Configuration"
local domain="${CONFIGURED_DOMAIN:-cmlite.org}"
local template_dir="$SCRIPT_DIR/nginx/conf.d"
local templates_found=0
for template in "$template_dir"/*.conf.template; do
[[ -f "$template" ]] || continue
templates_found=$((templates_found + 1))
local conf_file="${template%.template}"
local conf_name
conf_name=$(basename "$conf_file")
# Substitute ${DOMAIN} while preserving nginx variables ($host, $scheme, etc.)
sed "s/\${DOMAIN}/$domain/g" "$template" > "$conf_file"
success "Generated $conf_name"
done
if [[ $templates_found -eq 0 ]]; then
warn "No nginx .conf.template files found — skipping"
else
success "Generated $templates_found nginx configs for domain: $domain"
info "Restart nginx to apply: docker compose restart nginx"
fi
}
# =============================================================================
# Homepage services.yaml
# =============================================================================
# Read a variable from .env with fallback default
read_env_var() {
local key=$1 default=$2
local val
val=$(grep "^${key}=" "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2-)
echo "${val:-$default}"
}
generate_services_yaml() {
local domain="${CONFIGURED_DOMAIN:-cmlite.org}"
# Read embed proxy ports from .env (supports multi-instance port customization)
local rc_port; rc_port=$(read_env_var ROCKETCHAT_EMBED_PORT 8891)
local jitsi_port; jitsi_port=$(read_env_var JITSI_EMBED_PORT 8893)
mkdir -p "$(dirname "$SERVICES_YAML")"
cat > "$SERVICES_YAML" << YAML
---
# Homepage Services Configuration — Generated by config.sh
# Production tab: public URLs via $domain
# Local tab: localhost URLs with ports
- Production - Core:
- Admin GUI:
icon: mdi-view-dashboard
href: "https://app.$domain"
description: Application dashboard and public pages
container: changemaker-v2-admin
- API:
icon: mdi-api
href: "https://api.$domain"
description: V2 REST API
container: changemaker-v2-api
- Media API:
icon: mdi-video
href: "https://media.$domain"
description: Video library and streaming
container: changemaker-media-api
- Main Site:
icon: mdi-web
href: "https://$domain"
description: Documentation and marketing site
container: mkdocs-site-server-changemaker
- Production - Tools:
- Code Server:
icon: mdi-code-braces
href: "https://code.$domain"
description: VS Code in the browser
container: code-server-changemaker
- NocoDB:
icon: mdi-database
href: "https://db.$domain"
description: Database browser (read-only)
container: changemaker-v2-nocodb
- MkDocs (Live):
icon: mdi-book-open-page-variant
href: "https://docs.$domain"
description: Live documentation with hot reload
container: mkdocs-changemaker
- Mini QR:
icon: mdi-qrcode
href: "https://qr.$domain"
description: QR code generator
container: mini-qr
- Excalidraw:
icon: mdi-draw
href: "https://draw.$domain"
description: Collaborative whiteboard
container: excalidraw-changemaker
- Vaultwarden:
icon: mdi-lock
href: "https://vault.$domain"
description: Password manager (Bitwarden-compatible)
container: vaultwarden-changemaker
- Gancio:
icon: mdi-calendar-multiple
href: "https://events.$domain"
description: Event management platform
container: gancio-changemaker
- Rocket.Chat:
icon: mdi-chat
href: "https://chat.$domain"
description: Team communication platform
container: rocketchat-changemaker
- Jitsi Meet:
icon: mdi-video
href: "https://meet.$domain"
description: Video conferencing
container: jitsi-web-changemaker
- Production - Integrations:
- Listmonk:
icon: mdi-email-newsletter
href: "https://listmonk.$domain"
description: Newsletter and mailing list manager
container: listmonk-app
- MailHog:
icon: mdi-email-check
href: "https://mail.$domain"
description: Email capture for testing
container: mailhog-changemaker
- n8n:
icon: mdi-robot-industrial
href: "https://n8n.$domain"
description: Workflow automation platform
container: n8n-changemaker
- Gitea:
icon: mdi-git
href: "https://git.$domain"
description: Git repository hosting
container: gitea-changemaker
- Production - Infrastructure:
- Nginx:
icon: mdi-web-box
href: "#"
description: Reverse proxy (subdomain routing)
container: changemaker-v2-nginx
- PostgreSQL:
icon: mdi-database-outline
href: "#"
description: Primary database (V2)
container: changemaker-v2-postgres
- Redis:
icon: mdi-database-sync
href: "#"
description: Cache, rate limiting, job queues
container: redis-changemaker
- Production - Monitoring:
- Grafana:
icon: mdi-chart-box
href: "https://grafana.$domain"
description: Monitoring dashboards
container: grafana-changemaker
- Prometheus:
icon: mdi-chart-line
href: "https://prometheus.$domain"
description: Metrics collection
container: prometheus-changemaker
- Alertmanager:
icon: mdi-bell-alert
href: "https://alertmanager.$domain"
description: Alert routing
container: alertmanager-changemaker
- Gotify:
icon: mdi-cellphone-message
href: "https://gotify.$domain"
description: Push notifications
container: gotify-changemaker
- cAdvisor:
icon: mdi-docker
href: "https://cadvisor.$domain"
description: Container metrics
container: cadvisor-changemaker
- Node Exporter:
icon: mdi-server
href: "#"
description: Host system metrics
container: node-exporter-changemaker
- Redis Exporter:
icon: mdi-database-export
href: "#"
description: Redis metrics exporter
container: redis-exporter-changemaker
# ─────────────────────────────────────────────────
# LOCAL DEVELOPMENT
# ─────────────────────────────────────────────────
- Local - Core:
- Admin GUI:
icon: mdi-view-dashboard
href: "http://localhost:3000"
description: Application dashboard (port 3000)
container: changemaker-v2-admin
- API:
icon: mdi-api
href: "http://localhost:4000"
description: V2 REST API (port 4000)
container: changemaker-v2-api
- Media API:
icon: mdi-video
href: "http://localhost:4100"
description: Video library API (port 4100)
container: changemaker-media-api
- Main Site:
icon: mdi-web
href: "http://localhost:4004"
description: Documentation site (port 4004)
container: mkdocs-site-server-changemaker
- Homepage:
icon: mdi-home
href: "http://localhost:3010"
description: This dashboard (port 3010)
container: homepage-changemaker
- Local - Tools:
- Code Server:
icon: mdi-code-braces
href: "http://localhost:8888"
description: VS Code in the browser (port 8888)
container: code-server-changemaker
- NocoDB:
icon: mdi-database
href: "http://localhost:8091"
description: Database browser (port 8091)
container: changemaker-v2-nocodb
- MkDocs (Live):
icon: mdi-book-open-page-variant
href: "http://localhost:4003"
description: Live documentation (port 4003)
container: mkdocs-changemaker
- Mini QR:
icon: mdi-qrcode
href: "http://localhost:8089"
description: QR code generator (port 8089)
container: mini-qr
- Excalidraw:
icon: mdi-draw
href: "http://localhost:8090"
description: Collaborative whiteboard (port 8090)
container: excalidraw-changemaker
- Vaultwarden:
icon: mdi-lock
href: "http://localhost:8445"
description: Password manager (port 8445)
container: vaultwarden-changemaker
- Gancio:
icon: mdi-calendar-multiple
href: "http://localhost:8092"
description: Event management (port 8092)
container: gancio-changemaker
- Rocket.Chat:
icon: mdi-chat
href: "http://localhost:$rc_port"
description: Team chat (port $rc_port)
container: rocketchat-changemaker
- Jitsi Meet:
icon: mdi-video
href: "http://localhost:$jitsi_port"
description: Video conferencing (port $jitsi_port)
container: jitsi-web-changemaker
- Local - Integrations:
- Listmonk:
icon: mdi-email-newsletter
href: "http://localhost:9001"
description: Newsletter manager (port 9001)
container: listmonk-app
- MailHog:
icon: mdi-email-check
href: "http://localhost:8025"
description: Email capture UI (port 8025)
container: mailhog-changemaker
- n8n:
icon: mdi-robot-industrial
href: "http://localhost:5678"
description: Workflow automation (port 5678)
container: n8n-changemaker
- Gitea:
icon: mdi-git
href: "http://localhost:3030"
description: Git repository hosting (port 3030)
container: gitea-changemaker
- Local - Infrastructure:
- Nginx:
icon: mdi-web-box
href: "#"
description: Reverse proxy (port 80)
container: changemaker-v2-nginx
- PostgreSQL:
icon: mdi-database-outline
href: "#"
description: Primary database (port 5433)
container: changemaker-v2-postgres
- Redis:
icon: mdi-database-sync
href: "#"
description: Cache and job queues (port 6379)
container: redis-changemaker
- Local - Monitoring:
- Grafana:
icon: mdi-chart-box
href: "http://localhost:3005"
description: Monitoring dashboards (port 3005)
container: grafana-changemaker
- Prometheus:
icon: mdi-chart-line
href: "http://localhost:9090"
description: Metrics collection (port 9090)
container: prometheus-changemaker
- Alertmanager:
icon: mdi-bell-alert
href: "http://localhost:9093"
description: Alert routing (port 9093)
container: alertmanager-changemaker
- Gotify:
icon: mdi-cellphone-message
href: "http://localhost:8889"
description: Push notifications (port 8889)
container: gotify-changemaker
- cAdvisor:
icon: mdi-docker
href: "http://localhost:8086"
description: Container metrics (port 8086)
container: cadvisor-changemaker
- Node Exporter:
icon: mdi-server
href: "http://localhost:9100/metrics"
description: Host system metrics (port 9100)
container: node-exporter-changemaker
- Redis Exporter:
icon: mdi-database-export
href: "http://localhost:9121/metrics"
description: Redis metrics (port 9121)
container: redis-exporter-changemaker
YAML
success "Generated services.yaml for Homepage dashboard"
}
# =============================================================================
# Documentation Site Reset
# =============================================================================
configure_docs_reset() {
header "Documentation Site"
if prompt_yes_no "Reset documentation site to baseline? (keeps header & tracking)"; then
touch "$SCRIPT_DIR/mkdocs/.reset-docs-on-startup"
success "Docs reset scheduled for first startup"
info "Custom code (header, analytics, hooks, assets) will be preserved"
else
info "Keeping existing documentation content"
fi
}
# =============================================================================
# Directory Permissions
# =============================================================================
fix_container_permissions() {
header "Container Directory Permissions"
local -a dirs=(
"configs/code-server/.config:Code Server config"
"configs/code-server/.local:Code Server local data"
"mkdocs:MkDocs root"
"mkdocs/docs:MkDocs source docs"
"mkdocs/overrides:MkDocs template overrides"
"mkdocs/.cache:MkDocs cache"
"mkdocs/site:MkDocs built site"
"assets/uploads:Listmonk uploads"
"assets/images:Shared images"
"assets/icons:Homepage icons"
"media/local/inbox:Media upload inbox"
"media/local/photos:Media photos"
"media/local/thumbnails:Video thumbnails"
"media/public:Public media files"
"local-files:n8n local files"
"data:NAR import data"
"data/upgrade:Upgrade trigger directory"
)
local errors=0
for entry in "${dirs[@]}"; do
local dir_path="$SCRIPT_DIR/${entry%%:*}"
local dir_desc="${entry#*:}"
mkdir -p "$dir_path"
if chmod 775 "$dir_path" 2>/dev/null; then
success "$dir_desc"
else
warn "$dir_desc — could not set permissions (may need sudo)"
((errors++))
fi
done
echo ""
if [[ $errors -eq 0 ]]; then
success "All directories ready"
else
warn "Some directories may need: sudo chown -R $(id -u):$(id -g) $SCRIPT_DIR"
fi
}
# =============================================================================
# Upgrade Watcher (systemd)
# =============================================================================
install_upgrade_watcher() {
header "System Upgrade Watcher"
info "The upgrade watcher lets you trigger upgrades from the admin Settings page."
info "It installs a systemd path watcher that monitors for trigger files."
echo ""
# Ensure upgrade IPC directory exists
mkdir -p "$SCRIPT_DIR/data/upgrade"
local unit_src="$SCRIPT_DIR/scripts/systemd"
if [[ ! -f "$unit_src/changemaker-upgrade.path" ]] || [[ ! -f "$unit_src/changemaker-upgrade.service" ]]; then
warn "Systemd unit templates not found in scripts/systemd/ — skipping"
UPGRADE_WATCHER="skipped"
return
fi
if ! command -v systemctl &>/dev/null; then
warn "systemctl not found — skipping (not a systemd host?)"
UPGRADE_WATCHER="skipped"
return
fi
if prompt_yes_no "Install the upgrade watcher (requires sudo)?"; then
# Generate units with correct paths substituted
local tmp_path tmp_service
tmp_path=$(mktemp)
tmp_service=$(mktemp)
sed -e "s|__PROJECT_DIR__|$SCRIPT_DIR|g" "$unit_src/changemaker-upgrade.path" > "$tmp_path"
sed -e "s|__PROJECT_DIR__|$SCRIPT_DIR|g" -e "s|__USER__|$(whoami)|g" "$unit_src/changemaker-upgrade.service" > "$tmp_service"
if sudo cp "$tmp_path" /etc/systemd/system/changemaker-upgrade.path \
&& sudo cp "$tmp_service" /etc/systemd/system/changemaker-upgrade.service \
&& sudo systemctl daemon-reload \
&& sudo systemctl enable --now changemaker-upgrade.path; then
success "Upgrade watcher installed and enabled"
UPGRADE_WATCHER="yes"
else
warn "Failed to install systemd units (sudo may have failed)"
warn "Install manually later:"
echo -e " ${CYAN}cd ~/changemaker.lite && sudo ./scripts/systemd/install.sh${NC}"
UPGRADE_WATCHER="manual"
fi
rm -f "$tmp_path" "$tmp_service"
else
info "Skipped. You can install it later from Settings > System."
UPGRADE_WATCHER="skipped"
fi
}
# =============================================================================
# Automated Backups (systemd)
# =============================================================================
install_backup_timer() {
header "Automated Backups"
info "Daily automated backups protect against data loss."
info "Backs up PostgreSQL databases + uploads to ./backups/ with 30-day retention."
echo ""
local unit_src="$SCRIPT_DIR/scripts/systemd"
if [[ ! -f "$unit_src/changemaker-backup.timer" ]] || [[ ! -f "$unit_src/changemaker-backup.service" ]]; then
warn "Systemd backup unit templates not found in scripts/systemd/ — skipping"
BACKUP_TIMER="skipped"
return
fi
if ! command -v systemctl &>/dev/null; then
warn "systemctl not found — skipping (not a systemd host?)"
BACKUP_TIMER="skipped"
return
fi
if prompt_yes_no "Install daily automated backups (requires sudo)?" "y"; then
local tmp_timer tmp_service
tmp_timer=$(mktemp)
tmp_service=$(mktemp)
sed -e "s|__PROJECT_DIR__|$SCRIPT_DIR|g" "$unit_src/changemaker-backup.timer" > "$tmp_timer"
sed -e "s|__PROJECT_DIR__|$SCRIPT_DIR|g" -e "s|__USER__|$(whoami)|g" "$unit_src/changemaker-backup.service" > "$tmp_service"
if sudo cp "$tmp_timer" /etc/systemd/system/changemaker-backup.timer \
&& sudo cp "$tmp_service" /etc/systemd/system/changemaker-backup.service \
&& sudo systemctl daemon-reload \
&& sudo systemctl enable --now changemaker-backup.timer; then
success "Daily backup timer installed and enabled (runs at 02:00)"
BACKUP_TIMER="yes"
else
warn "Failed to install systemd units (sudo may have failed)"
warn "Install manually later:"
echo -e " ${CYAN}sudo cp scripts/systemd/changemaker-backup.* /etc/systemd/system/${NC}"
echo -e " ${CYAN}sudo systemctl daemon-reload && sudo systemctl enable --now changemaker-backup.timer${NC}"
BACKUP_TIMER="manual"
fi
rm -f "$tmp_timer" "$tmp_service"
else
info "Skipped. Run backups manually: ./scripts/backup.sh"
BACKUP_TIMER="skipped"
fi
}
# =============================================================================
# Summary & Next Steps
# =============================================================================
print_summary() {
header "Configuration Complete"
echo -e " ${BOLD}Domain:${NC} ${CONFIGURED_DOMAIN:-cmlite.org}"
echo -e " ${BOLD}Admin email:${NC} (see .env: INITIAL_ADMIN_EMAIL)"
echo -e " ${BOLD}Admin password:${NC} [set]"
echo -e " ${BOLD}SMTP:${NC} ${SMTP_MODE:-mailhog}"
echo -e " ${BOLD}Media Manager:${NC} ${MEDIA_ENABLED:-no}"
echo -e " ${BOLD}Payments:${NC} ${PAYMENTS_ENABLED:-no}"
echo -e " ${BOLD}Listmonk sync:${NC} ${LISTMONK_SYNC:-no}"
echo -e " ${BOLD}Rocket.Chat:${NC} ${CHAT_ENABLED:-no}"
echo -e " ${BOLD}Gancio sync:${NC} ${GANCIO_SYNC:-no}"
echo -e " ${BOLD}Jitsi Meet:${NC} ${MEET_ENABLED:-no}"
echo -e " ${BOLD}SMS Campaigns:${NC} ${SMS_ENABLED:-no}"
echo -e " ${BOLD}Monitoring:${NC} ${MONITORING_ENABLED:-no}"
echo -e " ${BOLD}Docs Comments:${NC} ${DOCS_COMMENTS_ENABLED:-no}"
echo -e " ${BOLD}Bunker Ops:${NC} ${BUNKER_OPS_ENABLED:-no}"
echo -e " ${BOLD}Pangolin:${NC} ${PANGOLIN_CONFIGURED:-no}"
echo -e " ${BOLD}Upgrade watcher:${NC} ${UPGRADE_WATCHER:-skipped}"
echo -e " ${BOLD}Backup timer:${NC} ${BACKUP_TIMER:-skipped}"
echo -e " ${BOLD}Secrets:${NC} 22 auto-generated"
echo ""
echo -e " ${DIM}Config file: $ENV_FILE${NC}"
}
print_next_steps() {
echo ""
echo -e "${BOLD}${BLUE}══════════════════════════════════════${NC}"
echo -e "${BOLD}${BLUE} Next Steps${NC}"
echo -e "${BOLD}${BLUE}══════════════════════════════════════${NC}"
echo ""
if [[ "$INSTALL_MODE" == "release" ]]; then
# Release mode: simpler instructions (production images, auto-migration)
echo -e " ${BOLD}1.${NC} Start all services:"
echo -e " ${CYAN}docker compose up -d${NC}"
echo ""
echo -e " Pre-built images will be pulled from the registry (~2 min first time)."
echo -e " Database migrations and seeding run automatically on startup."
echo ""
echo -e " ${BOLD}2.${NC} Access the application:"
echo -e " Admin GUI: ${CYAN}http://localhost:3000${NC}"
echo -e " API: ${CYAN}http://localhost:4000${NC}"
echo ""
echo -e " ${BOLD}3.${NC} Check status:"
echo -e " ${CYAN}docker compose ps${NC}"
echo -e " ${CYAN}docker compose logs -f api --tail 20${NC}"
echo ""
else
# Source mode: existing instructions
echo -e " ${BOLD}1.${NC} Start core services:"
echo -e " ${CYAN}docker compose up -d v2-postgres redis api admin${NC}"
echo ""
echo -e " ${BOLD}2.${NC} Run database setup:"
echo -e " ${CYAN}docker compose exec api npx prisma migrate deploy${NC}"
echo -e " ${CYAN}docker compose exec api npx prisma db seed${NC}"
echo ""
echo -e " ${BOLD}3.${NC} Access the application:"
echo -e " Admin GUI: ${CYAN}http://localhost:3000${NC}"
echo -e " API: ${CYAN}http://localhost:4000${NC}"
echo ""
echo -e " ${BOLD}4.${NC} Optional — start additional services:"
echo -e " ${CYAN}docker compose up -d nginx${NC} # Reverse proxy"
echo -e " ${CYAN}docker compose up -d media-api${NC} # Video library"
echo -e " ${CYAN}docker compose up -d listmonk-app${NC} # Newsletters"
echo -e " ${CYAN}docker compose up -d rocketchat${NC} # Team chat"
echo -e " ${CYAN}docker compose up -d jitsi-web jitsi-prosody jitsi-jicofo jitsi-jvb${NC} # Video calls"
echo -e " ${CYAN}docker compose up -d homepage${NC} # Service dashboard"
echo ""
echo -e " ${BOLD}5.${NC} Or start everything at once (monitoring included if enabled above):"
echo -e " ${CYAN}docker compose up -d${NC}"
echo ""
fi
echo -e " ${YELLOW}IMPORTANT: Change your admin password after first login!${NC}"
echo -e " ${YELLOW}JITSI: Ensure UDP port 10000 is open in your firewall for video/audio.${NC}"
echo ""
}
# =============================================================================
# Main
# =============================================================================
main() {
print_banner
check_prerequisites
initialize_env
configure_domain
configure_admin
generate_all_secrets
configure_smtp
configure_features
configure_pangolin
configure_cors
generate_nginx_configs
generate_services_yaml
configure_docs_reset
fix_container_permissions
install_upgrade_watcher
install_backup_timer
# Release mode: auto-set production defaults
if [[ "$INSTALL_MODE" == "release" ]]; then
header "Release Mode Settings"
update_env_var "IMAGE_TAG" "latest"
update_env_var "NODE_ENV" "production"
# Ensure monitoring is included if user opted in
if [[ "${MONITORING_ENABLED:-no}" == "yes" ]]; then
update_env_var "COMPOSE_PROFILES" "monitoring"
fi
success "Set IMAGE_TAG=latest, NODE_ENV=production (pre-built images)"
fi
print_summary
print_next_steps
}
main "$@"