b5bbc1f9ae
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>
268 lines
13 KiB
Bash
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."
|