Files
karim b5bbc1f9ae Add vps-install.sh: Stack-Deployment auf nacktem VPS (ohne Proxmox/LXC)
Installiert Docker, entfernt System-MTA (Port 25), deployt denselben Stack
direkt auf einem Debian-VPS; Self-signed-Cert + erstes Postfach vor-geseedet,
DKIM pro Domain, SnappyMail-Provisionierung, Abschluss mit DNS/PTR/Rspamd-PW.
curl-Einzeiler-tauglich.

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

268 lines
13 KiB
Bash

#!/usr/bin/env bash
# =============================================================================
# docker-mailserver Stack auf einem NACKTEN VPS (Debian) — ohne Proxmox/LXC
# -----------------------------------------------------------------------------
# Auf dem VPS als root ausführen:
# bash <(curl -fsSL https://git.kgva.ch/karim/DOCKERMAILSERVER-LXC/raw/branch/main/vps-install.sh)
#
# Installiert Docker, deployt mailserver + Admin-API + Admin-UI + SnappyMail
# + Rspamd, legt erstes Postfach + DKIM an. (Kein pct, keine LXC-Erstellung.)
# =============================================================================
set -Eeuo pipefail
# ---------------------------------------------------------------------------
# Einstellungen (per ENV oder interaktiv)
# ---------------------------------------------------------------------------
MAIL_FQDN="${MAIL_FQDN:-}"
MAIL_DOMAIN="${MAIL_DOMAIN:-}"
MAIL_DOMAINS="${MAIL_DOMAINS:-}"
FIRST_EMAIL="${FIRST_EMAIL:-}"
FIRST_PASSWORD="${FIRST_PASSWORD:-}"
BRAND="${BRAND:-}"
WEBMAIL_FQDN="${WEBMAIL_FQDN:-}"
ADMIN_FQDN="${ADMIN_FQDN:-}"
ADMIN_ALLOWED_EMAILS="${ADMIN_ALLOWED_EMAILS:-}"
SUPABASE_URL="${SUPABASE_URL:-}"
SUPABASE_ANON_KEY="${SUPABASE_ANON_KEY:-}"
RSPAMD_PASSWORD="${RSPAMD_PASSWORD:-}"
ADMIN_PORT="${ADMIN_PORT:-8080}"
WEBMAIL_PORT="${WEBMAIL_PORT:-8888}"
RSPAMD_PORT="${RSPAMD_PORT:-11334}"
TIMEZONE="${TZ:-Europe/Zurich}"
ENABLE_CLAMAV="${ENABLE_CLAMAV:-0}"
ENABLE_FAIL2BAN="${ENABLE_FAIL2BAN:-1}"
DMS_IMAGE="${DMS_IMAGE:-ghcr.io/docker-mailserver/docker-mailserver:latest}"
DEPLOY_DIR="${DEPLOY_DIR:-/opt/dms-stack}"
REPO_ARCHIVE="${REPO_ARCHIVE:-https://git.kgva.ch/karim/DOCKERMAILSERVER-LXC/archive/main.tar.gz}"
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
[[ $EUID -eq 0 ]] || die "Bitte als root ausführen."
command -v apt-get >/dev/null 2>&1 || die "Kein Debian/Ubuntu (apt fehlt)."
# --- Grundpakete ---
export DEBIAN_FRONTEND=noninteractive
msg_info "Installiere Grundpakete ..."
apt-get update -qq
apt-get install -y -qq ca-certificates curl gnupg openssl jq tar >/dev/null
# --- Stack-Ordner: neben dem Skript ODER selbst herunterladen ---
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 "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."
tar -xzf "$DL/repo.tar.gz" -C "$DL"
STACK_DIR="$(dirname "$(find "$DL" -maxdepth 4 -type f -name docker-compose.yml -path '*/stack/*' | head -1)")"
fi
[[ -f "$STACK_DIR/docker-compose.yml" ]] || die "stack/ nicht gefunden."
msg_ok "stack/ unter $STACK_DIR"
# ---------------------------------------------------------------------------
# Dialog
# ---------------------------------------------------------------------------
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 · VPS-Installer (Debian) ║
╚══════════════════════════════════════════════════════╝${CL}
EOF
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" "webmail.${MAIL_DOMAIN}")"
[[ -z "$ADMIN_FQDN" ]] && ADMIN_FQDN="$(ask "Admin-UI-Domain" "mailadmin.${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)")"
[[ -z "$RSPAMD_PASSWORD" ]] && RSPAMD_PASSWORD="$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 24)"
cat <<EOF
${GN}── Zusammenfassung ─────────────────────────────${CL}
Mail-FQDN ......... $MAIL_FQDN
Mail-Domains ...... $MAIL_DOMAINS
Erstes Postfach ... $FIRST_EMAIL
Brand ............. $BRAND
Webmail/Admin ..... $WEBMAIL_FQDN / $ADMIN_FQDN
Supabase .......... ${SUPABASE_URL:-<später in .env>}
${GN}────────────────────────────────────────────────${CL}
EOF
read -rp "$(echo -e " ${BL}?${CL} Fortfahren? [J/n] ")" go; [[ "${go:-J}" =~ ^[JjYy]?$ ]] || die "Abgebrochen."
# ---------------------------------------------------------------------------
# System-MTA entfernen (Postfix/Exim belegen Port 25) + Docker installieren
# ---------------------------------------------------------------------------
msg_info "Entferne System-MTA (Postfix/Exim) ..."
systemctl disable --now postfix exim4 >/dev/null 2>&1 || true
apt-get purge -y -qq postfix "exim4*" >/dev/null 2>&1 || true
if ! command -v docker >/dev/null 2>&1; then
msg_info "Installiere Docker ..."
curl -fsSL https://get.docker.com | sh >/dev/null 2>&1
systemctl enable --now docker >/dev/null 2>&1
fi
msg_ok "Docker bereit."
# ---------------------------------------------------------------------------
# Stack vorbereiten
# ---------------------------------------------------------------------------
msg_info "Bereite Stack vor ..."
mkdir -p "$DEPLOY_DIR"
cp -r "$STACK_DIR"/. "$DEPLOY_DIR"/
DMS_TAG="${DMS_IMAGE##*:}"
sed -i "s|^TZ=.*|TZ=${TIMEZONE}|" "$DEPLOY_DIR/mailserver.env"
sed -i "s|^ENABLE_CLAMAV=.*|ENABLE_CLAMAV=${ENABLE_CLAMAV}|" "$DEPLOY_DIR/mailserver.env"
sed -i "s|^ENABLE_FAIL2BAN=.*|ENABLE_FAIL2BAN=${ENABLE_FAIL2BAN}|" "$DEPLOY_DIR/mailserver.env"
cat > "$DEPLOY_DIR/.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
chmod 600 "$DEPLOY_DIR/.env"
# Rspamd-Controller-Passwort
mkdir -p "$DEPLOY_DIR/docker-data/dms/config/rspamd/override.d"
cat > "$DEPLOY_DIR/docker-data/dms/config/rspamd/override.d/worker-controller.inc" <<EOF
bind_socket = "*:11334";
password = "${RSPAMD_PASSWORD}";
enable_password = "${RSPAMD_PASSWORD}";
EOF
# Self-signed-Cert + erstes Postfach VOR dem Start (sonst bricht DMS ab)
msg_info "Erzeuge Self-signed-Zertifikat + erstes Postfach ..."
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
PW_HASH="$(printf '%s' "$FIRST_PASSWORD" | openssl passwd -6 -stdin)"
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 starten + DKIM + SnappyMail
# ---------------------------------------------------------------------------
msg_info "Baue & starte Stack (kann ein paar Minuten dauern) ..."
( cd "$DEPLOY_DIR" && docker compose up -d --build )
msg_info "Warte auf Mailserver ..."
for i in $(seq 1 60); do
docker exec mailserver ss -lnt 2>/dev/null | grep -q ":25 " && break
sleep 3
done
msg_info "Erzeuge DKIM pro Domain ..."
for d in $MAIL_DOMAINS; do
docker exec mailserver setup config dkim keysize 2048 domain "$d" >/dev/null 2>&1 \
&& msg_ok "DKIM $d" || msg_err "DKIM $d fehlgeschlagen"
done
DKIM_TXT="$(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: Wildcard-Domain -> mailserver, Shibui-Theme
SM_DEF="$DEPLOY_DIR/docker-data/snappymail/_data_/_default_"
for i in $(seq 1 30); do [ -f "$SM_DEF/domains/default.json" ] && break; sleep 2; done
if [ -f "$SM_DEF/domains/default.json" ]; then
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"' "$SM_DEF/domains/default.json" > "$SM_DEF/domains/default.json.tmp" \
&& mv "$SM_DEF/domains/default.json.tmp" "$SM_DEF/domains/default.json"
sed -i 's/^theme = .*/theme = "Shibui@custom"/' "$SM_DEF/configs/application.ini" 2>/dev/null || true
( cd "$DEPLOY_DIR" && docker compose restart snappymail >/dev/null 2>&1 ) || true
fi
PUB_IP="$(curl -fsSL https://ipv4.icanhazip.com 2>/dev/null || hostname -I | awk '{print $1}')"
# ---------------------------------------------------------------------------
# Abschluss
# ---------------------------------------------------------------------------
cat <<EOF
${GN}╔══════════════════════════════════════════════════════════════╗
║ docker-mailserver auf dem VPS ist eingerichtet 🎉 ║
╚══════════════════════════════════════════════════════════════╝${CL}
Server-IP .......... ${PUB_IP:-?}
Mailserver ......... $MAIL_FQDN
Erstes Postfach .... $FIRST_EMAIL
Webmail ............ http://${PUB_IP:-<ip>}:${WEBMAIL_PORT}
Admin-UI ........... http://${PUB_IP:-<ip>}:${ADMIN_PORT} (Supabase-Login)
Rspamd-UI .......... http://${PUB_IP:-<ip>}:${RSPAMD_PORT}
${RD}Rspamd-Passwort:${CL} ${RSPAMD_PASSWORD}
Verwaltung ......... cd ${DEPLOY_DIR} · docker compose ps
${YW}── DNS: Mailhost (einmalig) ──${CL}
A ${MAIL_FQDN}. IN A ${PUB_IP:-<server-ip>}
PTR ${PUB_IP:-<server-ip>} -> ${MAIL_FQDN} (in der Hetzner-Console: Server → rDNS)
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
echo "${DKIM_TXT:- (kein DKIM erzeugt)}"
cat <<EOF
${YW}── Hardening-Checkliste ──${CL}
[ ] Hetzner-Firewall: Mail-Ports (25,465,587,143,993) + 80/443 offen;
${ADMIN_PORT}/${WEBMAIL_PORT}/${RSPAMD_PORT} nur von deiner IP (oder per Reverse-Proxy/Tunnel).
[ ] Port 25 bei Hetzner entsperren (Support-Ticket) — für ausgehende Mail.
[ ] rDNS/PTR auf ${MAIL_FQDN} setzen (Hetzner-Console).
[ ] Echtes TLS: Let's-Encrypt-Cert für ${MAIL_FQDN} -> docker-data/certs/cert.pem|key.pem,
dann 'docker compose restart mailserver' (ersetzt self-signed).
[ ] Backups (Hetzner-Backups oder docker-data/ sichern).
Test: mail-tester.com · Details: README.md
EOF
msg_ok "Fertig."