be4906e2b5
- Bug: Variable HOSTNAME ist in der Shell bereits gesetzt (Proxmox-Hostname, z.B. "tanin") -> LXC bekam falschen Namen. Umbenannt zu CT_HOSTNAME + abgefragt. - Dialog fragt jetzt auch Hostname, CPU, RAM, Disk, Storage, Bridge. - Schönerer Dialog: Banner + Abschnitte (Container/Netzwerk/Mail/Branding/Supabase/Hardening), Prompt nach stderr (Werte sauber capturebar), Passwort-Eingabe verdeckt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
488 lines
24 KiB
Bash
488 lines
24 KiB
Bash
#!/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
|
||
# ACHTUNG: NICHT $HOSTNAME nutzen — das ist in der Shell bereits der Host-Name!
|
||
CT_HOSTNAME="${CT_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"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Interaktiver Dialog (Prompt -> stderr, Wert -> stdout, daher captureable)
|
||
# ---------------------------------------------------------------------------
|
||
ask() { local p="$1" d="${2:-}" v; read -rp "$(echo -e " ${BL}?${CL} ${p}${d:+ ${YW}[$d]${CL}}: ")" v; echo "${v:-$d}"; }
|
||
asks() { local p="$1" v; read -rsp "$(echo -e " ${BL}?${CL} ${p}: ")" v; echo >&2; echo "$v"; }
|
||
section() { echo -e "\n${GN}┌──${CL} ${BL}$1${CL}"; }
|
||
|
||
cat <<EOF
|
||
|
||
${GN} ╔══════════════════════════════════════════════════════╗
|
||
║ docker-mailserver · LXC-Installer für Proxmox VE ║
|
||
╚══════════════════════════════════════════════════════╝${CL}
|
||
Mailserver · Admin-UI · Webmail (SnappyMail) · Rspamd
|
||
EOF
|
||
|
||
section "Container (LXC)"
|
||
if [[ -z "$CTID" ]]; then CTID="$(pvesh get /cluster/nextid)"; fi
|
||
CTID="$(ask "Container-ID (CTID)" "$CTID")"
|
||
CT_HOSTNAME="$(ask "LXC-Hostname (Anzeigename)" "$CT_HOSTNAME")"
|
||
CORES="$(ask "CPU-Kerne" "$CORES")"
|
||
RAM_MB="$(ask "RAM (MB)" "$RAM_MB")"
|
||
DISK_GB="$(ask "Disk (GB)" "$DISK_GB")"
|
||
STORAGE="$(ask "Storage (rootfs)" "$STORAGE")"
|
||
|
||
section "Netzwerk"
|
||
BRIDGE="$(ask "Bridge" "$BRIDGE")"
|
||
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
|
||
|
||
section "Mail"
|
||
[[ -z "$MAIL_FQDN" ]] && MAIL_FQDN="$(ask "Mailserver-FQDN (z.B. mail.example.com)")"
|
||
[[ -n "$MAIL_FQDN" ]] || die "MAIL_FQDN ist Pflicht."
|
||
[[ -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")"
|
||
MAIL_DOMAINS="$(echo "$MAIL_DOMAIN $MAIL_DOMAINS" | tr ' ' '\n' | awk 'NF && !seen[$0]++' | tr '\n' ' ' | sed 's/ *$//')"
|
||
[[ -z "$FIRST_EMAIL" ]] && FIRST_EMAIL="$(ask "Erstes Postfach (E-Mail)" "admin@${MAIL_DOMAIN}")"
|
||
if [[ -z "$FIRST_PASSWORD" ]]; then
|
||
FIRST_PASSWORD="$(asks "Passwort für ${FIRST_EMAIL}")"
|
||
[[ -n "$FIRST_PASSWORD" ]] || die "Passwort darf nicht leer sein."
|
||
fi
|
||
|
||
section "Branding & Web-Domains (später im Admin editierbar)"
|
||
[[ -z "$BRAND" ]] && BRAND="$(ask "Anzeigename / Brand" "$MAIL_DOMAIN")"
|
||
[[ -z "$WEBMAIL_FQDN" ]] && WEBMAIL_FQDN="$(ask "Webmail-Domain (NPM)" "mail.${MAIL_DOMAIN}")"
|
||
[[ -z "$ADMIN_FQDN" ]] && ADMIN_FQDN="$(ask "Admin-UI-Domain (NPM)" "admin.${MAIL_DOMAIN}")"
|
||
|
||
section "Admin-Login (Supabase)"
|
||
[[ -z "$ADMIN_ALLOWED_EMAILS" ]] && ADMIN_ALLOWED_EMAILS="$(ask "Erlaubte Admin-E-Mail(s), Komma-getrennt" "$FIRST_EMAIL")"
|
||
[[ -z "$SUPABASE_URL" ]] && SUPABASE_URL="$(ask "Supabase URL (leer = später in .env)")"
|
||
[[ -z "$SUPABASE_ANON_KEY" ]] && SUPABASE_ANON_KEY="$(ask "Supabase anon key (leer = später in .env)")"
|
||
|
||
section "Hardening"
|
||
[[ -z "$NPM_IP" ]] && NPM_IP="$(ask "IP des Nginx Proxy Manager (Web nur von dort; leer = offen)")"
|
||
[[ -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 .......... $CT_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 "$CT_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."
|