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>
This commit is contained in:
+267
@@ -0,0 +1,267 @@
|
||||
#!/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."
|
||||
Reference in New Issue
Block a user