Files
DOCKERMAILSERVER-LXC/dms-lxc.sh
T
karim 4f0f7e62f1 Doku: Supabase-Admin-Setup klarer (Dialog-Output + README Abschnitt 5)
- Tool legt Admin-User nicht an -> Schritt-für-Schritt: User in Supabase
  (Auth → Users → Add user, Auto Confirm) + E-Mail in ADMIN_ALLOWED_EMAILS
- Deploy-Output zeigt Login-Vorbedingungen
- Hinweis auf optionalen Self-Service-Signup

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 02:34:46 +02:00

472 lines
23 KiB
Bash
Raw 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.
#!/usr/bin/env bash
# =============================================================================
# docker-mailserver (DMS) als LXC auf Proxmox VE
# -----------------------------------------------------------------------------
# Dieses Skript wird AUF DEM PROXMOX-HOST als root ausgeführt.
# Es legt einen UNPRIVILEGIERTEN LXC-Container (Debian 13) mit Nesting an,
# installiert Docker und deployt den kompletten Stack via docker compose:
# docker-mailserver + Admin-API + React-Admin-UI + SnappyMail.
#
# Der Ordner stack/ muss neben diesem Skript liegen.
#
# Aufruf: bash dms-lxc.sh
# oder: CTID=110 MAIL_FQDN=mail.example.com bash dms-lxc.sh
# =============================================================================
set -Eeuo pipefail
# ---------------------------------------------------------------------------
# Standard-Einstellungen (per ENV oder interaktiv überschreibbar)
# ---------------------------------------------------------------------------
CTID="${CTID:-}" # leer => nächste freie ID
HOSTNAME="${HOSTNAME:-mailserver}" # LXC-Hostname (Anzeigename)
MAIL_FQDN="${MAIL_FQDN:-}" # z.B. mail.example.com (PFLICHT)
MAIL_DOMAIN="${MAIL_DOMAIN:-}" # primäre Domain, z.B. kgva.ch (leer => aus FQDN abgeleitet)
MAIL_DOMAINS="${MAIL_DOMAINS:-}" # ALLE Mail-Domains (Leerzeichen-getrennt); leer => nur MAIL_DOMAIN
FIRST_EMAIL="${FIRST_EMAIL:-}" # erstes Postfach, z.B. admin@example.com
FIRST_PASSWORD="${FIRST_PASSWORD:-}" # Passwort für das erste Postfach
# Admin-UI / Webmail / Supabase
ADMIN_PORT="${ADMIN_PORT:-8080}" # Port der Admin-UI (NPM zeigt hierauf)
WEBMAIL_PORT="${WEBMAIL_PORT:-8888}" # Port von SnappyMail (NPM zeigt hierauf)
RSPAMD_PORT="${RSPAMD_PORT:-11334}" # Rspamd Web-UI (NPM zeigt hierauf)
ADMIN_ALLOWED_EMAILS="${ADMIN_ALLOWED_EMAILS:-}" # Komma-getrennte Admin-E-Mails (Supabase-Login)
SUPABASE_URL="${SUPABASE_URL:-}" # https://xxxx.supabase.co
SUPABASE_ANON_KEY="${SUPABASE_ANON_KEY:-}" # Supabase anon/public key
# Branding / Web-Domains (Erst-Befüllung, später in der Admin-UI editierbar)
BRAND="${BRAND:-}" # Anzeigename im Dashboard
WEBMAIL_FQDN="${WEBMAIL_FQDN:-}" # Webmail-Domain (NPM)
ADMIN_FQDN="${ADMIN_FQDN:-}" # Admin-UI-Domain (NPM)
# Hardening
NPM_IP="${NPM_IP:-}" # IP des Nginx Proxy Manager; schränkt Web-Ports per PVE-Firewall darauf ein (leer = offen, mit Warnung)
HARDEN_FIREWALL="${HARDEN_FIREWALL:-1}" # 1 = Proxmox-CT-Firewall-Regeln schreiben
RSPAMD_PASSWORD="${RSPAMD_PASSWORD:-}" # leer => wird generiert
# Container-Ressourcen (5 Container + Rspamd -> 4 GB empfohlen)
CORES="${CORES:-2}"
RAM_MB="${RAM_MB:-4096}" # 4 GB; mit ClamAV besser 6144+
DISK_GB="${DISK_GB:-20}" # nach erwartetem Mailvolumen wählen
SWAP_MB="${SWAP_MB:-512}"
# Storage / Template / Netz
OS_TEMPLATE="${OS_TEMPLATE:-debian-13-standard}" # Debian 13 (Trixie); Fallback auf 12 automatisch
STORAGE="${STORAGE:-local-lvm}" # Storage für rootfs
TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}" # Storage für CT-Template
BRIDGE="${BRIDGE:-vmbr0}"
NET_IP="${NET_IP:-dhcp}" # "dhcp" oder z.B. "192.168.1.50/24"
NET_GW="${NET_GW:-}" # Gateway, nur bei statischer IP
TIMEZONE="${TIMEZONE:-Europe/Zurich}"
# Funktionen / Optionen
ENABLE_CLAMAV="${ENABLE_CLAMAV:-0}" # 1 = Virenscan (braucht ~1GB RAM extra)
ENABLE_FAIL2BAN="${ENABLE_FAIL2BAN:-1}"
DMS_IMAGE="${DMS_IMAGE:-ghcr.io/docker-mailserver/docker-mailserver:latest}"
# ---------------------------------------------------------------------------
# Hübsche Ausgabe
# ---------------------------------------------------------------------------
RD=$'\033[01;31m'; GN=$'\033[1;92m'; YW=$'\033[33m'; BL=$'\033[1;34m'; CL=$'\033[m'
msg_info() { echo -e " ${YW}${CL} $1"; }
msg_ok() { echo -e " ${GN}${CL} $1"; }
msg_err() { echo -e " ${RD}${CL} $1" >&2; }
die() { msg_err "$1"; exit 1; }
trap 'msg_err "Fehler in Zeile $LINENO. Abbruch."' ERR
# ---------------------------------------------------------------------------
# Pre-Flight Checks
# ---------------------------------------------------------------------------
[[ $EUID -eq 0 ]] || die "Bitte als root auf dem Proxmox-Host ausführen."
command -v pct >/dev/null 2>&1 || die "'pct' nicht gefunden läuft das Skript wirklich auf einem Proxmox-Host?"
command -v pveam >/dev/null 2>&1 || die "'pveam' nicht gefunden kein Proxmox VE?"
command -v openssl >/dev/null 2>&1 || die "'openssl' nicht gefunden (für Passwort-Hash) 'apt install openssl'."
command -v curl >/dev/null 2>&1 || die "'curl' nicht gefunden 'apt install curl'."
# ---------------------------------------------------------------------------
# Stack-Ordner: neben dem Skript ODER per curl-Einzeiler selbst herunterladen
# curl-Einzeiler: bash <(curl -fsSL <RAW-URL>/dms-lxc.sh)
# Privates Repo: GIT_TOKEN=<token> bash <(curl ... )
# ---------------------------------------------------------------------------
REPO_ARCHIVE="${REPO_ARCHIVE:-https://git.kgva.ch/karim/DOCKERMAILSERVER-LXC/archive/main.tar.gz}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd || echo /tmp)"
STACK_DIR="${STACK_DIR:-$SCRIPT_DIR/stack}"
if [[ ! -f "$STACK_DIR/docker-compose.yml" ]]; then
msg_info "stack/ nicht gefunden lade Repo von $REPO_ARCHIVE ..."
DL="$(mktemp -d)"
AUTH=(); [[ -n "${GIT_TOKEN:-}" ]] && AUTH=(-H "Authorization: token ${GIT_TOKEN}")
curl -fsSL "${AUTH[@]}" "$REPO_ARCHIVE" -o "$DL/repo.tar.gz" || die "Download fehlgeschlagen (privates Repo? GIT_TOKEN setzen)."
tar -xzf "$DL/repo.tar.gz" -C "$DL" || die "Entpacken fehlgeschlagen."
FOUND="$(find "$DL" -maxdepth 4 -type f -name docker-compose.yml -path '*/stack/*' 2>/dev/null | head -1)"
[[ -n "$FOUND" ]] || die "stack/ im heruntergeladenen Archiv nicht gefunden."
STACK_DIR="$(dirname "$FOUND")"
msg_ok "Repo geladen, stack/ unter $STACK_DIR"
fi
[[ -f "$STACK_DIR/docker-compose.yml" ]] || die "stack/docker-compose.yml nicht gefunden unter $STACK_DIR"
# ---------------------------------------------------------------------------
# Interaktive Abfragen (nur wenn nicht per ENV gesetzt)
# ---------------------------------------------------------------------------
ask() { local p="$1" d="${2:-}" v; read -rp "$(echo -e "${BL}?${CL} $p ${d:+[$d] }")" v; echo "${v:-$d}"; }
if [[ -z "$CTID" ]]; then CTID="$(pvesh get /cluster/nextid)"; fi
CTID="$(ask "Container-ID (CTID)" "$CTID")"
[[ -z "$MAIL_FQDN" ]] && MAIL_FQDN="$(ask "Mailserver-FQDN (z.B. mail.example.com)")"
[[ -n "$MAIL_FQDN" ]] || die "MAIL_FQDN ist Pflicht."
# Domain aus FQDN ableiten (alles nach dem ersten Punkt) falls nicht gesetzt
[[ -z "$MAIL_DOMAIN" ]] && MAIL_DOMAIN="${MAIL_FQDN#*.}"
MAIL_DOMAIN="$(ask "Primäre Mail-Domain" "$MAIL_DOMAIN")"
[[ -z "$MAIL_DOMAINS" ]] && MAIL_DOMAINS="$(ask "Alle Mail-Domains (Leerzeichen-getrennt)" "$MAIL_DOMAIN")"
# Primäre Domain sicher enthalten, Duplikate raus
MAIL_DOMAINS="$(echo "$MAIL_DOMAIN $MAIL_DOMAINS" | tr ' ' '\n' | awk 'NF && !seen[$0]++' | tr '\n' ' ' | sed 's/ *$//')"
# Branding + Web-Domains (Defaults aus der primären Domain abgeleitet)
[[ -z "$BRAND" ]] && BRAND="$(ask "Anzeigename / Brand (Dashboard)" "$MAIL_DOMAIN")"
[[ -z "$WEBMAIL_FQDN" ]] && WEBMAIL_FQDN="$(ask "Webmail-Domain (NPM-Proxy-Host)" "mail.${MAIL_DOMAIN}")"
[[ -z "$ADMIN_FQDN" ]] && ADMIN_FQDN="$(ask "Admin-UI-Domain (NPM-Proxy-Host)" "admin.${MAIL_DOMAIN}")"
[[ -z "$FIRST_EMAIL" ]] && FIRST_EMAIL="$(ask "Erstes Postfach (E-Mail)" "admin@${MAIL_DOMAIN}")"
if [[ -z "$FIRST_PASSWORD" ]]; then
read -rsp "$(echo -e "${BL}?${CL} Passwort für ${FIRST_EMAIL}: ")" FIRST_PASSWORD; echo
[[ -n "$FIRST_PASSWORD" ]] || die "Passwort darf nicht leer sein."
fi
NET_IP="$(ask "IP (dhcp oder CIDR z.B. 192.168.1.50/24)" "$NET_IP")"
if [[ "$NET_IP" != "dhcp" && -z "$NET_GW" ]]; then
NET_GW="$(ask "Gateway (für statische IP)")"
fi
# --- Admin-UI / Supabase ---
[[ -z "$ADMIN_ALLOWED_EMAILS" ]] && ADMIN_ALLOWED_EMAILS="$(ask "Admin-E-Mail(s) für UI-Login (Supabase, Komma-getrennt)" "$FIRST_EMAIL")"
[[ -z "$SUPABASE_URL" ]] && SUPABASE_URL="$(ask "Supabase URL (leer = später in .env eintragen)")"
[[ -z "$SUPABASE_ANON_KEY" ]] && SUPABASE_ANON_KEY="$(ask "Supabase anon key (leer = später in .env eintragen)")"
# --- Hardening ---
[[ -z "$NPM_IP" ]] && NPM_IP="$(ask "IP des Nginx Proxy Manager (Web-Ports nur von dort; leer = offen)")"
# Rspamd-Controller-Passwort generieren, falls nicht gesetzt
[[ -z "$RSPAMD_PASSWORD" ]] && RSPAMD_PASSWORD="$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 24)"
cat <<EOF
${GN}── Zusammenfassung ─────────────────────────────${CL}
CTID .............. $CTID
Hostname .......... $HOSTNAME
Mail-FQDN ......... $MAIL_FQDN
Mail-Domains ...... $MAIL_DOMAINS
Erstes Postfach ... $FIRST_EMAIL
Ressourcen ........ ${CORES} vCPU / ${RAM_MB}MB RAM / ${DISK_GB}GB Disk
Storage ........... $STORAGE (Template: $TEMPLATE_STORAGE)
Netz .............. $BRIDGE / $NET_IP ${NET_GW:+gw $NET_GW}
Admin-UI .......... Port $ADMIN_PORT (Login: $ADMIN_ALLOWED_EMAILS)
Webmail ........... Port $WEBMAIL_PORT (SnappyMail)
Rspamd-UI ......... Port $RSPAMD_PORT (Passwort wird generiert)
Supabase .......... ${SUPABASE_URL:-<später in .env eintragen>}
Firewall .......... $([[ "$HARDEN_FIREWALL" == 1 ]] && echo "PVE-CT-Firewall an${NPM_IP:+, Web nur von $NPM_IP}" || echo aus)
ClamAV ............ $([[ "$ENABLE_CLAMAV" == 1 ]] && echo an || echo aus)
Fail2ban .......... $([[ "$ENABLE_FAIL2BAN" == 1 ]] && echo an || echo aus)
${GN}────────────────────────────────────────────────${CL}
EOF
read -rp "$(echo -e "${BL}?${CL} Fortfahren? [J/n] ")" go; [[ "${go:-J}" =~ ^[JjYy]?$ ]] || die "Abgebrochen."
# ---------------------------------------------------------------------------
# CT-Template sicherstellen
# ---------------------------------------------------------------------------
msg_info "Suche $OS_TEMPLATE Template ..."
pveam update >/dev/null 2>&1 || true
TEMPLATE="$(pveam available --section system | awk -v t="$OS_TEMPLATE" '$2 ~ t {print $2}' | sort -V | tail -1)"
if [[ -z "$TEMPLATE" ]]; then
msg_err "$OS_TEMPLATE nicht verfügbar fällt auf debian-12-standard zurück."
TEMPLATE="$(pveam available --section system | awk '/debian-12-standard/{print $2}' | sort -V | tail -1)"
fi
[[ -n "$TEMPLATE" ]] || die "Kein passendes Debian-Template in 'pveam available' gefunden."
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
msg_info "Lade Template $TEMPLATE herunter ..."
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
fi
TEMPLATE_REF="${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}"
msg_ok "Template bereit: $TEMPLATE"
# ---------------------------------------------------------------------------
# Netzwerk-String bauen
# ---------------------------------------------------------------------------
FW_FLAG=""
[[ "$HARDEN_FIREWALL" == 1 ]] && FW_FLAG=",firewall=1"
if [[ "$NET_IP" == "dhcp" ]]; then
NET="name=eth0,bridge=${BRIDGE},ip=dhcp${FW_FLAG}"
else
NET="name=eth0,bridge=${BRIDGE},ip=${NET_IP}${NET_GW:+,gw=${NET_GW}}${FW_FLAG}"
fi
# ---------------------------------------------------------------------------
# LXC erstellen (unprivilegiert) + Nesting/keyctl für Docker
# ---------------------------------------------------------------------------
msg_info "Erstelle LXC $CTID ..."
pct create "$CTID" "$TEMPLATE_REF" \
--hostname "$HOSTNAME" \
--cores "$CORES" \
--memory "$RAM_MB" \
--swap "$SWAP_MB" \
--rootfs "${STORAGE}:${DISK_GB}" \
--net0 "$NET" \
--features nesting=1,keyctl=1 \
--unprivileged 1 \
--onboot 1 \
--ostype debian \
--timezone "$TIMEZONE" \
--description "docker-mailserver (DMS) — ${MAIL_FQDN}"
msg_ok "LXC $CTID erstellt."
# ---------------------------------------------------------------------------
# Proxmox-CT-Firewall: Mail-Ports offen, Web/Rspamd nur von NPM (falls gesetzt)
# (greift nur, wenn die Datacenter/Node-Firewall aktiviert ist — sonst inert)
# ---------------------------------------------------------------------------
if [[ "$HARDEN_FIREWALL" == 1 ]]; then
msg_info "Schreibe Proxmox-CT-Firewall-Regeln ..."
WEBSRC=""; [[ -n "$NPM_IP" ]] && WEBSRC=" -source ${NPM_IP}"
mkdir -p /etc/pve/firewall
cat > "/etc/pve/firewall/${CTID}.fw" <<EOF
[OPTIONS]
enable: 1
policy_in: DROP
policy_out: ACCEPT
[RULES]
# SSH/Console
IN ACCEPT -p tcp -dport 22
# Mail (von überall erreichbar)
IN ACCEPT -p tcp -dport 25
IN ACCEPT -p tcp -dport 465
IN ACCEPT -p tcp -dport 587
IN ACCEPT -p tcp -dport 143
IN ACCEPT -p tcp -dport 993
# Web/Admin/Webmail/Rspamd — nur vom Nginx Proxy Manager (falls NPM_IP gesetzt)
IN ACCEPT -p tcp -dport ${ADMIN_PORT}${WEBSRC}
IN ACCEPT -p tcp -dport ${WEBMAIL_PORT}${WEBSRC}
IN ACCEPT -p tcp -dport ${RSPAMD_PORT}${WEBSRC}
EOF
msg_ok "Firewall-Regeln geschrieben (${NPM_IP:+Web nur von $NPM_IP}${NPM_IP:-Web offen NPM_IP setzen für Einschränkung})."
fi
msg_info "Starte Container ..."
pct start "$CTID"
# Auf Netzwerk warten
for i in {1..30}; do
pct exec "$CTID" -- getent hosts deb.debian.org >/dev/null 2>&1 && break
sleep 2
done
msg_ok "Container läuft."
# ---------------------------------------------------------------------------
# Basis-Setup + Docker im Container installieren
# ---------------------------------------------------------------------------
msg_info "Installiere Updates & Docker im Container ..."
pct exec "$CTID" -- bash -c '
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq ca-certificates curl gnupg openssl jq >/dev/null
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
. /etc/os-release
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian ${VERSION_CODENAME} stable" \
> /etc/apt/sources.list.d/docker.list
apt-get update -qq
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null
systemctl enable --now docker >/dev/null 2>&1
'
msg_ok "Docker installiert."
# ---------------------------------------------------------------------------
# Stack-Ordner vorbereiten (.env + mailserver.env patchen) und übertragen
# ---------------------------------------------------------------------------
DEPLOY_DIR="/opt/dms-stack"
DMS_TAG="${DMS_IMAGE##*:}"
msg_info "Bereite Stack vor ..."
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
cp -r "$STACK_DIR" "$TMP/stack"
# mailserver.env an Optionen anpassen
sed -i "s|^TZ=.*|TZ=${TIMEZONE}|" "$TMP/stack/mailserver.env"
sed -i "s|^ENABLE_CLAMAV=.*|ENABLE_CLAMAV=${ENABLE_CLAMAV}|" "$TMP/stack/mailserver.env"
sed -i "s|^ENABLE_FAIL2BAN=.*|ENABLE_FAIL2BAN=${ENABLE_FAIL2BAN}|" "$TMP/stack/mailserver.env"
# .env für docker compose schreiben
cat > "$TMP/stack/.env" <<EOF
MAIL_FQDN=${MAIL_FQDN}
MAIL_DOMAIN=${MAIL_DOMAIN}
MAIL_DOMAINS=${MAIL_DOMAINS}
BRAND=${BRAND}
WEBMAIL_FQDN=${WEBMAIL_FQDN}
ADMIN_FQDN=${ADMIN_FQDN}
DMS_TAG=${DMS_TAG}
ADMIN_PORT=${ADMIN_PORT}
WEBMAIL_PORT=${WEBMAIL_PORT}
RSPAMD_PORT=${RSPAMD_PORT}
ADMIN_ALLOWED_EMAILS=${ADMIN_ALLOWED_EMAILS}
SUPABASE_URL=${SUPABASE_URL}
SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}
EOF
# Rspamd-Controller mit Passwort schützen (Web-UI)
mkdir -p "$TMP/stack/docker-data/dms/config/rspamd/override.d"
cat > "$TMP/stack/docker-data/dms/config/rspamd/override.d/worker-controller.inc" <<EOF
bind_socket = "*:11334";
password = "${RSPAMD_PASSWORD}";
enable_password = "${RSPAMD_PASSWORD}";
EOF
# Verzeichnis per tar in den Container übertragen (pct push kann nur Dateien)
tar -C "$TMP/stack" -czf "$TMP/stack.tar.gz" .
pct exec "$CTID" -- mkdir -p "$DEPLOY_DIR"
pct push "$CTID" "$TMP/stack.tar.gz" /tmp/stack.tar.gz
pct exec "$CTID" -- tar -xzf /tmp/stack.tar.gz -C "$DEPLOY_DIR"
pct exec "$CTID" -- rm -f /tmp/stack.tar.gz
# Hardening: .env (Supabase-Keys) nur für root lesbar
pct exec "$CTID" -- chmod 600 "$DEPLOY_DIR/.env"
msg_ok "Stack übertragen nach $DEPLOY_DIR"
# ---------------------------------------------------------------------------
# TLS-Cert + erstes Postfach VOR dem Start vor-seeden
# (DMS bricht sonst ab: ohne Cert -> TLS-Fehler, ohne Konto -> Shutdown nach 120s,
# und Port 25 öffnet erst NACH dem ersten Konto -> sonst Deadlock beim Warten.)
# ---------------------------------------------------------------------------
msg_info "Erzeuge Self-signed-Zertifikat (Fallback bis NPM/Let's Encrypt) ..."
pct exec "$CTID" -- bash -c "
set -e
mkdir -p $DEPLOY_DIR/docker-data/certs $DEPLOY_DIR/docker-data/dms/config
if [ ! -f $DEPLOY_DIR/docker-data/certs/cert.pem ]; then
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
-keyout $DEPLOY_DIR/docker-data/certs/key.pem \
-out $DEPLOY_DIR/docker-data/certs/cert.pem \
-subj '/CN=$MAIL_FQDN' -addext 'subjectAltName=DNS:$MAIL_FQDN' >/dev/null 2>&1
fi
"
msg_info "Lege erstes Postfach an: $FIRST_EMAIL"
# Passwort-Hash auf dem Host erzeugen (Passwort geht nie in den Container).
PW_HASH="$(printf '%s' "$FIRST_PASSWORD" | openssl passwd -6 -stdin)"
pct exec "$CTID" -- bash -c "grep -q '^${FIRST_EMAIL}|' $DEPLOY_DIR/docker-data/dms/config/postfix-accounts.cf 2>/dev/null || \
printf '%s\n' '${FIRST_EMAIL}|{SHA512-CRYPT}${PW_HASH}' >> $DEPLOY_DIR/docker-data/dms/config/postfix-accounts.cf"
# ---------------------------------------------------------------------------
# Stack bauen & starten + DKIM
# ---------------------------------------------------------------------------
msg_info "Baue & starte Stack (Image-Pull/Build kann einige Minuten dauern) ..."
pct exec "$CTID" -- bash -c "cd $DEPLOY_DIR && docker compose up -d --build"
# Warten bis Mailserver auf Port 25 lauscht (kommt jetzt, da Konto+Cert vorhanden)
msg_info "Warte auf Mailserver-Start ..."
for i in {1..60}; do
pct exec "$CTID" -- bash -c 'docker exec mailserver ss -lnt 2>/dev/null | grep -q ":25 "' && break
sleep 3
done
msg_info "Erzeuge DKIM-Schlüssel (RSA 2048) pro Domain ..."
for d in $MAIL_DOMAINS; do
pct exec "$CTID" -- bash -c "docker exec mailserver setup config dkim keysize 2048 domain '$d'" \
|| msg_err "DKIM für $d fehlgeschlagen später manuell nachholen."
done
# alle DKIM-DNS-Records einsammeln (rspamd legt je Domain eine .dns.txt an)
DKIM_TXT="$(pct exec "$CTID" -- bash -c "for f in $DEPLOY_DIR/docker-data/dms/config/rspamd/dkim/*.dns.txt; do [ -f \"\$f\" ] && echo \"; --- \$(basename \"\$f\" .dns.txt) ---\" && cat \"\$f\" && echo; done 2>/dev/null" || true)"
# --- SnappyMail provisionieren: Wildcard-Domain -> mailserver-Container, Shibui als Default ---
msg_info "Provisioniere SnappyMail (alle Domains -> mailserver, Shibui-Theme) ..."
SM_DEF="$DEPLOY_DIR/docker-data/snappymail/_data_/_default_"
for i in {1..30}; do
pct exec "$CTID" -- bash -c "[ -f '$SM_DEF/domains/default.json' ]" && break
sleep 2
done
pct exec "$CTID" -- bash -c '
set -e
D="$1"; STACK="$2"
[ -f "$D/domains/default.json" ] || exit 0
jq ".IMAP.host=\"mailserver\" | .IMAP.port=143 | .IMAP.type=2 | .IMAP.ssl.allow_self_signed=true
| .SMTP.host=\"mailserver\" | .SMTP.port=587 | .SMTP.type=2 | .SMTP.useAuth=true | .SMTP.ssl.allow_self_signed=true
| .Sieve.host=\"mailserver\"" "$D/domains/default.json" > "$D/domains/default.json.tmp"
mv "$D/domains/default.json.tmp" "$D/domains/default.json"
sed -i "s/^theme = .*/theme = \"Shibui@custom\"/" "$D/configs/application.ini" 2>/dev/null || true
cd "$STACK" && docker compose restart snappymail >/dev/null 2>&1
' _ "$SM_DEF" "$DEPLOY_DIR" || msg_err "SnappyMail-Provisionierung übersprungen (kann manuell erfolgen)."
# Container-IP ermitteln
CT_IP="$(pct exec "$CTID" -- bash -c "hostname -I | awk '{print \$1}'" 2>/dev/null || true)"
# ---------------------------------------------------------------------------
# Abschluss / DNS-Hinweise
# ---------------------------------------------------------------------------
cat <<EOF
${GN}╔══════════════════════════════════════════════════════════════╗
║ docker-mailserver ist eingerichtet 🎉 ║
╚══════════════════════════════════════════════════════════════╝${CL}
Container .......... CTID $CTID (IP: ${CT_IP:-unbekannt})
Mailserver ......... $MAIL_FQDN
Erstes Postfach .... $FIRST_EMAIL
Admin-UI ........... http://${CT_IP:-<ip>}:${ADMIN_PORT} (Supabase-Login)
Webmail ............ http://${CT_IP:-<ip>}:${WEBMAIL_PORT} (SnappyMail)
Rspamd-UI .......... http://${CT_IP:-<ip>}:${RSPAMD_PORT} (Passwort unten)
Verwaltung ......... pct enter $CTID → cd ${DEPLOY_DIR}
${BL}Tipp:${CL} Im Nginx Proxy Manager als Proxy-Hosts anlegen (HTTPS):
admin.${MAIL_DOMAIN} -> ${CT_IP:-<ip>}:${ADMIN_PORT}
mail.${MAIL_DOMAIN} -> ${CT_IP:-<ip>}:${WEBMAIL_PORT}
rspamd.${MAIL_DOMAIN} -> ${CT_IP:-<ip>}:${RSPAMD_PORT}
${RD}Rspamd-Web-UI-Passwort:${CL} ${RSPAMD_PASSWORD}
(steht auch in ${DEPLOY_DIR}/docker-data/dms/config/rspamd/override.d/worker-controller.inc)
${YW}── Admin-Login (Supabase) — VOR dem ersten Login nötig ──${CL}
Das Tool legt den Admin-User NICHT an. Einmalig im Supabase-Dashboard:
1) Authentication → Users → Add user: eine deiner Admin-E-Mails + Passwort, „Auto Confirm" an
(aktuell erlaubt: ${ADMIN_ALLOWED_EMAILS:-<noch nicht gesetzt>})
2) SUPABASE_URL/ANON_KEY müssen in ${DEPLOY_DIR}/.env stehen${SUPABASE_URL:+ ✔}
3) dann: cd ${DEPLOY_DIR} && docker compose up -d && docker compose restart admin-ui admin-api
Details: README, Abschnitt 5.
${YW}── DNS: einmal für den Mailhost ──${CL}
A-Record: ${MAIL_FQDN}. IN A <öffentliche IP>
PTR/rDNS: <öffentliche IP> -> ${MAIL_FQDN} (beim Hoster/ISP setzen!)
${YW}── DNS: pro Mail-Domain (MX/SPF/DMARC) ──${CL}
EOF
for d in $MAIL_DOMAINS; do
cat <<EOF
[${d}]
MX ${d}. IN MX 10 ${MAIL_FQDN}.
SPF ${d}. IN TXT "v=spf1 mx ~all"
DMARC _dmarc.${d}. IN TXT "v=DMARC1; p=quarantine; rua=mailto:postmaster@${d}"
EOF
done
cat <<EOF
${YW}── DKIM (TXT) je Domain ──${CL}
EOF
if [[ -n "$DKIM_TXT" ]]; then
echo "$DKIM_TXT"
else
echo " (DKIM-Records noch nicht erzeugt siehe README, Abschnitt DKIM.)"
fi
cat <<EOF
${BL}Wichtig:${CL} Port 25 muss von deinem ISP/Hoster freigegeben sein und
per Portweiterleitung (25,465,587,143,993) zum Container ${CT_IP:-…} zeigen.
${YW}── Hardening-Checkliste ──${CL}
[ ] Echtes TLS: NPM-Zertifikat (DNS-Challenge) nach docker-data/certs/cert.pem|key.pem, dann
'docker compose restart mailserver' (ersetzt das Self-signed).
[ ] PVE-Firewall: $([[ "$HARDEN_FIREWALL" == 1 ]] && echo "CT-Regeln geschrieben${NPM_IP:+ (Web nur von $NPM_IP)}" || echo "AUS").
Wirkt nur, wenn die Datacenter/Node-Firewall aktiv ist (Datacenter → Firewall → Options → Enable).
[ ] Web-Ports (Admin/Webmail/Rspamd) nur über NPM/HTTPS erreichbar machen${NPM_IP:+ — NPM_IP gesetzt ✔}.
[ ] Backups: vzdump des CT + ${DEPLOY_DIR}/docker-data (Mail!).
[ ] DMARC nach Testphase auf p=reject verschärfen.
Details, TLS, Backup & Troubleshooting: siehe README.md
EOF
msg_ok "Fertig."