mailwolt-installer/install.sh

909 lines
35 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/usr/bin/env bash
(
export HISTFILE=
set +o history
set -euo pipefail
##############################################
# MailWolt #
# Bootstrap Installer v1.0 #
##############################################
# ===== CLI-Flags (-dev / -stag) =====
APP_ENV="${APP_ENV:-production}"
APP_DEBUG="${APP_DEBUG:-false}"
DEV_MODE=0
STAG_MODE=0
while [[ $# -gt 0 ]]; do
case "$1" in
-dev)
DEV_MODE=1
APP_ENV="local"
APP_DEBUG="true"
;;
-stag|-staging)
STAG_MODE=1
APP_ENV="staging"
APP_DEBUG="false"
;;
esac
shift
done
# ===== Branding & Pfade =====
APP_NAME="${APP_NAME:-MailWolt}"
APP_USER="${APP_USER:-mailwolt}"
APP_GROUP="${APP_GROUP:-www-data}"
APP_DIR="/var/www/${APP_USER}"
ADMIN_USER="${APP_USER}"
ADMIN_EMAIL="admin@localhost"
ADMIN_PASS="ChangeMe"
CONF_BASE="/etc/${APP_USER}"
CERT_DIR="${CONF_BASE}/ssl"
CERT="${CERT_DIR}/cert.pem"
KEY="${CERT_DIR}/key.pem"
NGINX_SITE="/etc/nginx/sites-available/${APP_USER}.conf"
NGINX_SITE_LINK="/etc/nginx/sites-enabled/${APP_USER}.conf"
DB_NAME="${DB_NAME:-${APP_USER}}"
DB_USER="${DB_USER:-${APP_USER}}"
DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}"
GIT_REPO="${GIT_REPO:-http://10.10.20.81:3000/boban/mailwolt.git}"
GIT_BRANCH="${GIT_BRANCH:-main}"
NODE_SETUP="${NODE_SETUP:-deb}"
# ===== Styling =====
GREEN="\033[1;32m"; YELLOW="\033[1;33m"; RED="\033[1;31m"; CYAN="\033[1;36m"; GREY="\033[0;90m"; NC="\033[0m"
BAR="──────────────────────────────────────────────────────────────────────────────"
header() {
echo -e "${CYAN}${BAR}${NC}"
echo -e "${CYAN} 888b d888 d8b 888 888 888 888 888 ${NC}"
echo -e "${CYAN} 8888b d8888 Y8P 888 888 o 888 888 888 ${NC}"
echo -e "${CYAN} 88888b.d88888 888 888 d8b 888 888 888 ${NC}"
echo -e "${CYAN} 888Y88888P888 8888b. 888 888 888 d888b 888 .d88b. 888 888888 ${NC}"
echo -e "${CYAN} 888 Y888P 888 '88b 888 888 888d88888b888 d88''88b 888 888 ${NC}"
echo -e "${CYAN} 888 Y8P 888 .d888888 888 888 88888P Y88888 888 888 888 888 ${NC}"
echo -e "${CYAN} 888 ' 888 888 888 888 888 8888P Y8888 Y88..88P 888 Y88b. ${NC}"
echo -e "${CYAN} 888 888 'Y888888 888 888 888P Y888 'Y88P' 888 'Y888 ${NC}"
echo -e "${CYAN}${BAR}${NC}"
echo
}
print_bootstrap_summary() {
local ip="$1"
local admin_user="$2"
local admin_pass="$3"
local GREEN="\033[1;32m"
local CYAN="\033[1;36m"
local GREY="\033[0;90m"
local YELLOW="\033[1;33m"
local RED="\033[1;31m"
local NC="\033[0m"
local BAR="${BAR:-──────────────────────────────────────────────────────────────────────────────}"
local scheme="http"
if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then scheme="https"; fi
echo
echo -e "${GREEN}${BAR}${NC}"
echo -e "${GREEN}${APP_NAME} Bootstrap erfolgreich abgeschlossen${NC}"
echo -e "${GREEN}${BAR}${NC}"
echo -e " Bootstrap-Login (nur für ERSTEN Login & Wizard):"
echo -e " User: ${YELLOW}${admin_user}${NC}"
echo -e " Passwort: ${RED}${admin_pass}${NC}"
echo
echo -e " Aufruf: ${CYAN}${scheme}://${ip}${NC}"
echo -e " Laravel Root: ${GREY}${APP_DIR}${NC}"
echo -e " Nginx Site: ${GREY}${NGINX_SITE}${NC}"
echo -e " Self-signed Cert: ${GREY}${CERT_DIR}/{cert.pem,key.pem}${NC}"
echo -e " Postfix/Dovecot Ports aktiv: ${GREY}25, 465, 587, 110, 995, 143, 993${NC}"
echo -e " Rspamd/OpenDKIM: ${GREY}aktiv (DKIM-Keys später im Wizard)${NC}"
echo -e " Monit (Watchdog): ${GREY}installiert, NICHT aktiviert${NC}"
echo -e "${GREEN}${BAR}${NC}"
echo
}
log() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
err() { echo -e "${RED}[x]${NC} $*"; }
require_root() { [ "$(id -u)" -eq 0 ] || { err "Bitte als root ausführen."; exit 1; }; }
detect_ip() {
local ip
ip="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')" || true
[[ -n "${ip:-}" ]] || ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
[[ -n "${ip:-}" ]] || { err "Konnte Server-IP nicht ermitteln."; exit 1; }
echo "$ip"
}
gen() { head -c 512 /dev/urandom | tr -dc 'A-Za-z0-9' | head -c "${1:-28}" || true; }
pw() { gen 28; }
short() { gen 16; }
# ===== Start =====
require_root
header
SERVER_IP="$(detect_ip)"
MAIL_HOSTNAME="${MAIL_HOSTNAME:-"bootstrap.local"}"
TZ="${TZ:-""}"
echo -e "${GREY}Server-IP erkannt: ${SERVER_IP}${NC}"
[ -n "$TZ" ] && { ln -fs "/usr/share/zoneinfo/${TZ}" /etc/localtime || true; }
log "Paketquellen aktualisieren…"
export DEBIAN_FRONTEND=noninteractive
apt-get update -y
# ---- MariaDB-Workaround ----
log "MariaDB-Workaround vorbereiten…"
mkdir -p /etc/mysql /etc/mysql/mariadb.conf.d
[ -f /etc/mysql/mariadb.cnf ] || echo '!include /etc/mysql/mariadb.conf.d/*.cnf' > /etc/mysql/mariadb.cnf
# ---- Basis-Pakete installieren ----
log "Pakete installieren… (dies kann einige Minuten dauern)"
export DEBIAN_FRONTEND=noninteractive
apt-get -y -o Dpkg::Options::="--force-confdef" \
-o Dpkg::Options::="--force-confold" install \
postfix postfix-mysql \
dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-mysql \
mariadb-server mariadb-client \
redis-server \
rspamd \
opendkim opendkim-tools \
nginx \
php php-fpm php-cli php-mbstring php-xml php-curl php-zip php-mysql php-redis php-gd unzip curl \
composer git \
certbot python3-certbot-nginx \
fail2ban \
ca-certificates rsyslog sudo openssl netcat-openbsd monit acl
NGINX_HTTP2_SUPPORTED=0
if nginx -V 2>&1 | grep -q http_v2; then
NGINX_HTTP2_SUPPORTED=1
log "Nginx: HTTP/2-Unterstützung vorhanden ✅"
else
warn "Nginx: HTTP/2-Modul nicht gefunden wechsle auf 'nginx-full'…"
apt-get install -y nginx-full || true
systemctl restart nginx || true
if nginx -V 2>&1 | grep -q http_v2; then
NGINX_HTTP2_SUPPORTED=1
log "Nginx: HTTP/2 jetzt verfügbar ✅"
else
warn "HTTP/2 weiterhin nicht verfügbar (verwende SSL ohne http2)."
fi
fi
if [ "$NGINX_HTTP2_SUPPORTED" = "1" ]; then
NGINX_HTTP2_SUFFIX=" http2"
else
NGINX_HTTP2_SUFFIX=""
fi
# ===== Verzeichnisse / User =====
log "Verzeichnisse und Benutzer anlegen…"
mkdir -p "${CERT_DIR}" /etc/postfix/sql /etc/dovecot/conf.d /etc/rspamd/local.d /var/mail/vhosts
id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail
chown -R vmail:vmail /var/mail
id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER"
usermod -a -G "$APP_GROUP" "$APP_USER"
# ===== Self-signed TLS (SAN = IP) =====
OSSL_CFG="${CERT_DIR}/openssl.cnf"
if [ ! -s "$CERT" ] || [ ! -s "$KEY" ]; then
log "Erzeuge Self-Signed TLS Zertifikat (SAN=IP:${SERVER_IP})…"
install -d -m 0750 -o root -g "${APP_USER}" "${CERT_DIR}"
cat > "$OSSL_CFG" <<CFG
[req]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn
[dn]
CN = ${SERVER_IP}
O = ${APP_NAME}
C = DE
[req_ext]
subjectAltName = @alt_names
[alt_names]
IP.1 = ${SERVER_IP}
CFG
openssl req -x509 -newkey rsa:2048 -days 825 -nodes \
-keyout "$KEY" -out "$CERT" -config "$OSSL_CFG"
chown root:"${APP_USER}" "$KEY" "$CERT"
chmod 640 "$KEY" "$CERT"
chmod 750 "${CERT_DIR}"
fi
DEV_USER="${SUDO_USER:-$USER}"
if command -v setfacl >/dev/null 2>&1; then
setfacl -m u:${DEV_USER}:x "${CONF_BASE}" "${CERT_DIR}" || true
setfacl -m u:${DEV_USER}:r "$CERT" "$KEY" || true
fi
# ===== MariaDB =====
log "MariaDB vorbereiten…"
systemctl enable --now mariadb
mysql -uroot <<SQL
CREATE DATABASE IF NOT EXISTS ${DB_NAME}
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost';
CREATE USER IF NOT EXISTS '${DB_USER}'@'127.0.0.1';
ALTER USER '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
ALTER USER '${DB_USER}'@'127.0.0.1' IDENTIFIED BY '${DB_PASS}';
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'127.0.0.1';
FLUSH PRIVILEGES;
SQL
# ===== Postfix =====
postconf -e "myhostname = ${MAIL_HOSTNAME}"
postconf -e "myorigin = \$myhostname"
postconf -e "mydestination = "
postconf -e "inet_interfaces = all"
postconf -e "inet_protocols = ipv4"
postconf -e "smtpd_banner = \$myhostname ESMTP"
postconf -e "smtpd_tls_cert_file = ${CERT}"
postconf -e "smtpd_tls_key_file = ${KEY}"
postconf -e "smtpd_tls_security_level = may"
postconf -e "smtp_tls_security_level = may"
postconf -e "smtpd_tls_received_header = yes"
postconf -e "disable_vrfy_command = yes"
postconf -e "smtpd_helo_required = yes"
postconf -e "milter_default_action = accept"
postconf -e "milter_protocol = 6"
postconf -e "smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
postconf -e "non_smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
postconf -e "smtpd_sasl_type = dovecot"
postconf -e "smtpd_sasl_path = private/auth"
postconf -e "smtpd_sasl_auth_enable = yes"
postconf -e "smtpd_sasl_security_options = noanonymous"
postconf -e "smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination"
postconf -e "smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination"
postconf -M "smtp/inet=smtp inet n - n - - smtpd -o smtpd_peername_lookup=no -o smtpd_timeout=30s"
postconf -M "submission/inet=submission inet n - n - - smtpd -o syslog_name=postfix/submission -o smtpd_peername_lookup=no -o smtpd_tls_security_level=encrypt -o smtpd_tls_auth_only=yes -o smtpd_sasl_auth_enable=yes -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
postconf -M "smtps/inet=smtps inet n - n - - smtpd -o syslog_name=postfix/smtps -o smtpd_peername_lookup=no -o smtpd_tls_wrappermode=yes -o smtpd_tls_auth_only=yes -o smtpd_sasl_auth_enable=yes -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
postconf -M "pickup/unix=pickup unix n - y 60 1 pickup"
postconf -M "cleanup/unix=cleanup unix n - y - 0 cleanup"
postconf -M "qmgr/unix=qmgr unix n - n 300 1 qmgr"
install -d -o root -g postfix -m 750 /etc/postfix/sql
install -o root -g postfix -m 640 /dev/null /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
cat > /etc/postfix/sql/mysql-virtual-mailbox-maps.cf <<CONF
hosts = 127.0.0.1
user = ${DB_USER}
password = ${DB_PASS}
dbname = ${DB_NAME}
# query = SELECT 1 FROM mail_users u JOIN domains d ON d.id = u.domain_id WHERE u.email = '%s' AND u.is_active = 1 AND d.is_active = 1 LIMIT 1;
CONF
chown root:postfix /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
chmod 640 /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
install -o root -g postfix -m 640 /dev/null /etc/postfix/sql/mysql-virtual-alias-maps.cf
cat > /etc/postfix/sql/mysql-virtual-alias-maps.cf <<CONF
hosts = 127.0.0.1
user = ${DB_USER}
password = ${DB_PASS}
dbname = ${DB_NAME}
# query = SELECT destination FROM mail_aliases a JOIN domains d ON d.id = a.domain_id WHERE a.source = '%s' AND a.is_active = 1 AND d.is_active = 1 LIMIT 1;
CONF
chown root:postfix /etc/postfix/sql/mysql-virtual-alias-maps.cf
chmod 640 /etc/postfix/sql/mysql-virtual-alias-maps.cf
systemctl restart postfix
systemctl enable --now postfix
# ===== Dovecot =====
log "Dovecot konfigurieren…"
cat > /etc/dovecot/dovecot.conf <<'CONF'
!include_try /etc/dovecot/conf.d/*.conf
CONF
cat > /etc/dovecot/conf.d/10-mail.conf <<'CONF'
protocols = imap pop3 lmtp
mail_location = maildir:/var/mail/vhosts/%d/%n
namespace inbox { inbox = yes }
mail_privileged_group = mail
CONF
cat > /etc/dovecot/conf.d/10-auth.conf <<'CONF'
disable_plaintext_auth = yes
auth_mechanisms = plain login
!include_try auth-sql.conf.ext
CONF
cat > /etc/dovecot/dovecot-sql.conf.ext <<CONF
driver = mysql
connect = host=127.0.0.1 dbname=${DB_NAME} user=${DB_USER} password=${DB_PASS}
default_pass_scheme = BLF-CRYPT
# password_query = SELECT email AS user, password_hash AS password FROM mail_users WHERE email = '%u' AND is_active = 1 LIMIT 1;
CONF
chown root:dovecot /etc/dovecot/dovecot-sql.conf.ext
chmod 640 /etc/dovecot/dovecot-sql.conf.ext
cat > /etc/dovecot/conf.d/auth-sql.conf.ext <<'CONF'
passdb { driver = sql args = /etc/dovecot/dovecot-sql.conf.ext }
userdb { driver = static args = uid=vmail gid=vmail home=/var/mail/vhosts/%d/%n }
CONF
sudo chown root:dovecot /etc/dovecot/conf.d/auth-sql.conf.ext
sudo chmod 640 /etc/dovecot/conf.d/auth-sql.conf.ext
cat > /etc/dovecot/conf.d/10-master.conf <<'CONF'
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp { mode = 0600 user = postfix group = postfix }
}
service auth {
unix_listener /var/spool/postfix/private/auth { mode = 0660 user = postfix group = postfix }
}
service imap-login {
inet_listener imap { port = 143 }
inet_listener imaps { port = 993 ssl = yes }
}
service pop3-login {
inet_listener pop3 { port = 110 }
inet_listener pop3s { port = 995 ssl = yes }
}
CONF
cat > /etc/dovecot/conf.d/10-ssl.conf <<CONF
ssl = required
ssl_cert = <${CERT}
ssl_key = <${KEY}
CONF
sudo mkdir -p /var/spool/postfix/private
sudo chown postfix:postfix /var/spool/postfix /var/spool/postfix/private
sudo chmod 0755 /var/spool/postfix /var/spool/postfix/private
sudo systemctl restart dovecot
sudo systemctl restart postfix
systemctl enable --now dovecot
# ===== Rspamd & OpenDKIM =====
log "Rspamd + OpenDKIM aktivieren…"
cat > /etc/rspamd/local.d/worker-controller.inc <<'CONF'
password = "admin";
bind_socket = "127.0.0.1:11334";
CONF
systemctl enable --now rspamd || true
cat > /etc/opendkim.conf <<'CONF'
Syslog yes
UMask 002
Mode sv
Socket inet:8891@127.0.0.1
Canonicalization relaxed/simple
On-BadSignature accept
On-Default accept
On-KeyNotFound accept
On-NoSignature accept
LogWhy yes
OversignHeaders From
# KeyTable / SigningTable werden nach dem Wizard gesetzt
CONF
systemctl enable --now opendkim || true
# ===== Redis =====
log "Redis absichern (Passwort setzen & nur localhost)…"
REDIS_CONF="/etc/redis/redis.conf"
REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}"
sed -i 's/^\s*#\?\s*bind .*/bind 127.0.0.1/' "$REDIS_CONF"
sed -i 's/^\s*#\?\s*protected-mode .*/protected-mode yes/' "$REDIS_CONF"
if grep -qE '^\s*#?\s*requirepass ' "$REDIS_CONF"; then
sed -i "s/^\s*#\?\s*requirepass .*/requirepass ${REDIS_PASS}/" "$REDIS_CONF"
else
printf "\nrequirepass %s\n" "${REDIS_PASS}" >> "$REDIS_CONF"
fi
systemctl enable --now redis-server
systemctl restart redis-server
# ===== Nginx =====
log "Nginx konfigurieren…"
rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default || true
detect_php_fpm_sock() {
for v in 8.3 8.2 8.1 8.0 7.4; do s="/run/php/php${v}-fpm.sock"; [ -S "$s" ] && { echo "$s"; return; }; done
[ -S "/run/php/php-fpm.sock" ] && { echo "/run/php/php-fpm.sock"; return; }
echo "127.0.0.1:9000"
}
PHP_FPM_SOCK="$(detect_php_fpm_sock)"
install -d -m 0755 /var/www/letsencrypt
if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then
cat > "${NGINX_SITE}" <<CONF
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
location ^~ /.well-known/acme-challenge/ { root /var/www/letsencrypt; allow all; }
return 301 https://\$host\$request_uri;
}
server {
listen 443 ssl${NGINX_HTTP2_SUFFIX};
listen [::]:443 ssl${NGINX_HTTP2_SUFFIX};
server_name _;
ssl_certificate ${CERT};
ssl_certificate_key ${KEY};
ssl_protocols TLSv1.2 TLSv1.3;
root ${APP_DIR}/public;
index index.php index.html;
access_log /var/log/nginx/${APP_USER}_ssl_access.log;
error_log /var/log/nginx/${APP_USER}_ssl_error.log;
client_max_body_size 25m;
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
location ~ \.php\$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:${PHP_FPM_SOCK}; }
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
location /ws {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host \$host;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
}
CONF
else
cat > "${NGINX_SITE}" <<CONF
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root ${APP_DIR}/public;
index index.php index.html;
access_log /var/log/nginx/${APP_USER}_access.log;
error_log /var/log/nginx/${APP_USER}_error.log;
client_max_body_size 25m;
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
location ~ \.php\$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:${PHP_FPM_SOCK}; }
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
}
CONF
fi
ln -sf "${NGINX_SITE}" "${NGINX_SITE_LINK}"
if nginx -t; then
systemctl enable --now nginx
systemctl reload nginx || true
else
echo "[x] Nginx-Konfiguration fehlerhaft bitte /var/log/nginx/* prüfen."
exit 1
fi
# ===== Laravel Projekt =====
log "Laravel bereitstellen…"
mkdir -p "$(dirname "$APP_DIR")"
chown -R "$APP_USER":"$APP_GROUP" "$(dirname "$APP_DIR")"
log "Git Repo vorbereiten…"
if [ "${GIT_REPO}" = "https://example.com/your-repo-placeholder.git" ]; then
if [ ! -d "${APP_DIR}" ] || [ -z "$(ls -A "$APP_DIR" 2>/dev/null || true)" ]; then
sudo -u "$APP_USER" -H bash -lc "cd /var/www && COMPOSER_ALLOW_SUPERUSER=0 composer create-project laravel/laravel ${APP_USER} --no-interaction"
fi
else
if [ ! -d "${APP_DIR}/.git" ]; then
sudo -u "$APP_USER" -H bash -lc "git clone --depth=1 -b ${GIT_BRANCH} ${GIT_REPO} ${APP_DIR}"
else
sudo -u "$APP_USER" -H bash -lc "
set -e
cd ${APP_DIR}
git checkout ${GIT_BRANCH} 2>/dev/null || git checkout -B ${GIT_BRANCH}
git fetch --depth=1 origin ${GIT_BRANCH}
if git merge-base --is-ancestor HEAD origin/${GIT_BRANCH}; then
git pull --ff-only
else
echo '[i] Non-fast-forward erkannt setze hart auf origin/${GIT_BRANCH}.' >&2
git reset --hard origin/${GIT_BRANCH}
git clean -fd
fi
"
fi
if [ -f "${APP_DIR}/composer.json" ]; then
if [ "${DEV_MODE}" = "1" ]; then
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-interaction --prefer-dist"
else
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-interaction --prefer-dist --no-dev"
fi
fi
fi
# ===== Node / Frontend =====
if [ -f "${APP_DIR}/package.json" ]; then
log "Node/NPM installieren…"
if command -v node >/dev/null 2>&1; then
NODE_MAJ=$(node -v | sed 's/^v//' | cut -d. -f1)
NODE_MIN=$(node -v | sed 's/^v//' | cut -d. -f2)
if [ "$NODE_MAJ" -lt 20 ] || { [ "$NODE_MAJ" -eq 20 ] && [ "$NODE_MIN" -lt 19 ]; }; then
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt-get install -y nodejs
fi
else
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt-get install -y nodejs
fi
# .env anlegen & APP_KEY
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && cp -n .env.example .env || true"
if ! grep -q '^APP_KEY=' "${APP_DIR}/.env"; then echo "APP_KEY=" >> "${APP_DIR}/.env"; fi
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force || true"
fi
# ===== .env füllen =====
ENV_FILE="${APP_DIR}/.env"
upsert_env () {
local key="$1" val="$2"
local esc_key esc_val
esc_key="$(printf '%s' "$key" | sed -e 's/[.[\*^$(){}+?|/]/\\&/g')"
esc_val="$(printf '%s' "$val" | sed -e 's/[&/]/\\&/g')"
if grep -qE "^[#[:space:]]*${esc_key}=" "$ENV_FILE"; then
sed -Ei "s|^[#[:space:]]*${esc_key}=.*|${key}=${esc_val}|g" "$ENV_FILE"
else
printf '%s=%s\n' "$key" "$val" >> "$ENV_FILE"
fi
}
if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then
upsert_env APP_URL "\"https://\${APP_HOST}\""
else
upsert_env APP_URL "\"http://\${APP_HOST}\""
fi
upsert_env APP_HOST "${SERVER_IP}"
upsert_env APP_ADMIN_USER "${ADMIN_USER}"
upsert_env APP_ADMIN_EMAIL "${ADMIN_EMAIL}"
upsert_env APP_ADMIN_PASS "${ADMIN_PASS}"
upsert_env APP_NAME "${APP_NAME}"
upsert_env APP_ENV "${APP_ENV}"
upsert_env APP_DEBUG "${APP_DEBUG}"
upsert_env DB_CONNECTION "mysql"
upsert_env DB_HOST "127.0.0.1"
upsert_env DB_PORT "3306"
upsert_env DB_DATABASE "${DB_NAME}"
upsert_env DB_USERNAME "${DB_USER}"
upsert_env DB_PASSWORD "${DB_PASS}"
# -------- WICHTIG: Cache-Store auf REDIS setzen (verhindert DB-Tabellenfehler) --------
upsert_env CACHE_SETTINGS_STORE "redis"
upsert_env CACHE_STORE "redis" # <- HIER geändert (vorher: database)
upsert_env CACHE_DRIVER "redis"
upsert_env CACHE_PREFIX "${APP_USER}_cache"
upsert_env SESSION_DRIVER "redis"
upsert_env REDIS_CLIENT "phpredis"
upsert_env REDIS_HOST "127.0.0.1"
upsert_env REDIS_PORT "6379"
upsert_env REDIS_PASSWORD "${REDIS_PASS}"
upsert_env REDIS_DB "0"
upsert_env REDIS_CACHE_DB "1"
upsert_env REDIS_CACHE_CONNECTION "cache"
upsert_env REDIS_CACHE_LOCK_CONNECTION "default"
# Reverb / Vite
upsert_env BROADCAST_DRIVER "reverb"
upsert_env QUEUE_CONNECTION "redis"
upsert_env REVERB_APP_ID "${APP_USER}"
upsert_env REVERB_APP_KEY "${APP_USER}-yhp47tbt1aebhr1fgvgj"
upsert_env REVERB_APP_SECRET "${APP_USER}-ulrdt9agwzkqwqsunbnb"
upsert_env REVERB_HOST "127.0.0.1"
upsert_env REVERB_PORT "443"
upsert_env REVERB_SCHEME "https"
upsert_env REVERB_PATH "/ws"
upsert_env VITE_REVERB_APP_KEY "\${REVERB_APP_KEY}"
upsert_env VITE_REVERB_PORT "\${REVERB_PORT}"
upsert_env VITE_REVERB_SCHEME "\${REVERB_SCHEME}"
upsert_env VITE_REVERB_PATH "\${REVERB_PATH}"
if [ "${DEV_MODE}" = "1" ]; then
sed -i '/^# --- MailWolt DEV/,/^# --- \/MailWolt DEV/d' "${ENV_FILE}"
cat >> "${ENV_FILE}" <<CONF
# --- MailWolt DEV ---
VITE_DEV_HOST=127.0.0.1
VITE_DEV_PORT=5173
VITE_HMR_PROTOCOL=wss
VITE_HMR_CLIENT_PORT=443
VITE_HMR_HOST=${APP_HOST}
VITE_DEV_ORIGIN=${APP_URL}
# --- /MailWolt DEV ---
CONF
cat > "${APP_DIR}/vite.config.js" <<'JS'
import { defineConfig, loadEnv } from 'vite'
import laravel from 'laravel-vite-plugin'
import tailwindcss from '@tailwindcss/vite'
export default ({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const host = env.VITE_DEV_HOST || '127.0.0.1'
const port = Number(env.VITE_DEV_PORT || 5173)
const origin = env.VITE_DEV_ORIGIN || env.APP_URL || 'https://localhost'
const hmrHost = env.VITE_HMR_HOST || (new URL(origin)).hostname
return defineConfig({
plugins: [laravel({ input: ['resources/css/app.css','resources/js/app.js'], refresh: true }), tailwindcss()],
server: { host, port, https:false, strictPort:true,
hmr:{ protocol: env.VITE_HMR_PROTOCOL || 'wss', host: hmrHost, clientPort:Number(env.VITE_HMR_CLIENT_PORT||443) },
origin
}
})
}
JS
chown "${APP_USER}:${APP_GROUP}" "${APP_DIR}/vite.config.js"
fi
# ===== Frontend Build =====
if [ -f "${APP_DIR}/package.json" ]; then
log "Frontend Build…"
if [ -f "${APP_DIR}/package-lock.json" ] || [ -f "${APP_DIR}/npm-shrinkwrap.json" ]; then
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm ci --no-audit --no-fund || npm install"
else
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm install"
fi
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build || true"
rm -f ${APP_DIR}/bootstrap/cache/*.php
fi
# ===== Rechte / PHP-FPM =====
APP_PW="${APP_PW:-changeme}"
if ! id -u "$APP_USER" >/dev/null 2>&1; then
adduser --disabled-password --gecos "" "$APP_USER"
echo "${APP_USER}:${APP_PW}" | chpasswd
fi
usermod -a -G "$APP_GROUP" "$APP_USER"
# Sichert, dass alle nötigen Ordner existieren (idempotent)
install -d -m 0775 "${APP_DIR}/storage" \
"${APP_DIR}/storage/framework" \
"${APP_DIR}/storage/framework/cache" \
"${APP_DIR}/storage/framework/cache/data" \
"${APP_DIR}/storage/framework/sessions" \
"${APP_DIR}/storage/framework/views" \
"${APP_DIR}/bootstrap/cache"
# Besitz & Rechte
chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR"
find "$APP_DIR" -type d -exec chmod 775 {} \;
find "$APP_DIR" -type f -exec chmod 664 {} \;
[ -f "$APP_DIR/artisan" ] && chmod 755 "$APP_DIR/artisan"
[ -d "$APP_DIR/vendor/bin" ] && chmod -R 755 "$APP_DIR/vendor/bin"
[ -d "$APP_DIR/node_modules/.bin" ] && chmod -R 755 "$APP_DIR/node_modules/.bin"
[ -f "$APP_DIR/node_modules/vite/bin/vite.js" ] && chmod 755 "$APP_DIR/node_modules/vite/bin/vite.js"
find "$APP_DIR" -type f -name "*.sh" -exec chmod 755 {} \;
chmod -R 775 "$APP_DIR"/storage "$APP_DIR"/bootstrap/cache
if command -v setfacl >/dev/null 2>&1; then
setfacl -R -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
setfacl -dR -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
fi
grep -q 'umask 002' /home/${APP_USER}/.profile 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.profile
grep -q 'umask 002' /home/${APP_USER}/.bashrc 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.bashrc
sudo -u "$APP_USER" -H bash -lc "npm config set umask 0002" >/dev/null 2>&1 || true
PHPV=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
FPM_POOL="/etc/php/${PHPV}/fpm/pool.d/www.conf"
if [ -f "$FPM_POOL" ]; then
sed -i 's/^;*listen\.owner.*/listen.owner = www-data/' "$FPM_POOL"
sed -i 's/^;*listen\.group.*/listen.group = www-data/' "$FPM_POOL"
sed -i 's/^;*listen\.mode.*/listen.mode = 0660/' "$FPM_POOL"
systemctl restart php${PHPV}-fpm || true
fi
IDE_USER="${SUDO_USER:-}"
if [ -n "$IDE_USER" ] && id "$IDE_USER" >/dev/null 2>&1 && command -v setfacl >/dev/null 2>&1; then
usermod -a -G "$APP_GROUP" "$IDE_USER" || true
setfacl -R -m u:${IDE_USER}:rwX "$APP_DIR"
setfacl -dR -m u:${IDE_USER}:rwX "$APP_DIR"
echo -e "${GREEN}[i]${NC} Benutzer '${IDE_USER}' wurde für Schreibzugriff freigeschaltet (ACL + Gruppe ${APP_GROUP})."
fi
# Webstack neu laden
systemctl reload nginx || true
systemctl restart php*-fpm || true
# ===== Reverb systemd =====
cat > /etc/systemd/system/mailwolt-ws.service <<EOF
[Unit]
Description=MailWolt WebSocket Backend
After=network.target
[Service]
Type=simple
Environment=NODE_ENV=production WS_PORT=8080
User=${APP_USER}
Group=${APP_GROUP}
WorkingDirectory=${APP_DIR}
ExecStart=/usr/bin/php artisan reverb:start --host=127.0.0.1 --port=8080 --no-interaction
Restart=always
RestartSec=2
StandardOutput=append:/var/log/mailwolt-ws.log
StandardError=append:/var/log/mailwolt-ws.log
KillSignal=SIGINT
TimeoutStopSec=15
[Install]
WantedBy=multi-user.target
EOF
chown root:root /etc/systemd/system/mailwolt-ws.service
chmod 644 /etc/systemd/system/mailwolt-ws.service
touch /var/log/mailwolt-ws.log
chown ${APP_USER}:${APP_GROUP} /var/log/mailwolt-ws.log
chmod 664 /var/log/mailwolt-ws.log
install -d -m 775 -o "${APP_USER}" -g "${APP_GROUP}" \
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache"
# ---- Laravel-Caches als APP_USER aufräumen (funktioniert jetzt ohne DB-Cache-Tabelle) ----
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan optimize:clear && php artisan config:cache"
# HINWEIS: Wenn du unbedingt DATABASE als Cache-Store willst, dann vor obiger Zeile:
# sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan cache:table && php artisan migrate --force"
systemctl daemon-reload
if sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan list --no-ansi 2>/dev/null | grep -qE '(^| )reverb:start( |$)'"; then
systemctl enable --now mailwolt-ws
else
systemctl disable --now mailwolt-ws >/dev/null 2>&1 || true
fi
# ===== Monit =====
log "Monit konfigurieren & starten…"
cat > /etc/monit/monitrc <<'EOF'
set daemon 60
set logfile syslog facility log_daemon
check process postfix with pidfile /var/spool/postfix/pid/master.pid
start program = "/bin/systemctl start postfix"
stop program = "/bin/systemctl stop postfix"
if failed port 25 protocol smtp then restart
if failed port 465 type tcp ssl then restart
if failed port 587 type tcp then restart
check process dovecot with pidfile /var/run/dovecot/master.pid
start program = "/bin/systemctl start dovecot"
stop program = "/bin/systemctl stop dovecot"
if failed port 143 type tcp then restart
if failed port 993 type tcp ssl then restart
if failed port 110 type tcp then restart
if failed port 995 type tcp ssl then restart
check process mariadb with pidfile /var/run/mysqld/mysqld.pid
start program = "/bin/systemctl start mariadb"
stop program = "/bin/systemctl stop mariadb"
if failed port 3306 type tcp then restart
check process redis-server with pidfile /run/redis/redis-server.pid
start program = "/bin/systemctl start redis-server"
stop program = "/bin/systemctl stop redis-server"
if failed port 6379 type tcp then restart
check process rspamd with pidfile /run/rspamd/rspamd.pid
start program = "/bin/systemctl start rspamd"
stop program = "/bin/systemctl stop rspamd"
if failed port 11332 type tcp then restart
check process opendkim with pidfile /run/opendkim/opendkim.pid
start program = "/bin/systemctl start opendkim"
stop program = "/bin/systemctl stop opendkim"
if failed port 8891 type tcp then restart
check process nginx with pidfile /run/nginx.pid
start program = "/bin/systemctl start nginx"
stop program = "/bin/systemctl stop nginx"
if failed port 80 type tcp then restart
if failed port 443 type tcp ssl then restart
check process mailwolt-ws matching "reverb:start"
start program = "/bin/systemctl start mailwolt-ws"
stop program = "/bin/systemctl stop mailwolt-ws"
if failed host 127.0.0.1 port 8080 type tcp for 2 cycles then restart
if 5 restarts within 5 cycles then timeout
EOF
chmod 600 /etc/monit/monitrc
monit -t && systemctl enable --now monit
monit reload
monit summary || true
# ===== Healthchecks =====
GREEN="\033[1;32m"; RED="\033[1;31m"; GREY="\033[0;90m"; NC="\033[0m"
ok(){ echo -e " [${GREEN}OK${NC}]"; }
fail(){ echo -e " [${RED}FAIL${NC}]"; }
echo "[+] Quick-Healthchecks…"
printf " • MariaDB … " ; mysqladmin ping --silent >/dev/null 2>&1 && ok || fail
printf " • Redis … " ; if command -v redis-cli >/dev/null 2>&1; then
if [ -n "${REDIS_PASS:-}" ] && [ "${REDIS_PASS}" != "null" ]; then
redis-cli -a "${REDIS_PASS}" ping 2>/dev/null | grep -q PONG && ok || fail
else
redis-cli ping 2>/dev/null | grep -q PONG && ok || fail
fi
else fail; fi
printf " • PHP-FPM … " ; if [[ "$PHP_FPM_SOCK" == 127.0.0.1:9000 ]]; then ss -ltn | grep -q ":9000 " && ok || fail; else [ -S "$PHP_FPM_SOCK" ] && ok || fail; fi
printf " • App … " ; if command -v curl >/dev/null 2>&1; then
if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then curl -skI "https://127.0.0.1" >/dev/null 2>&1 && ok || fail; else curl -sI "http://127.0.0.1" >/dev/null 2>&1 && ok || fail; fi
else echo -e " ${GREY}(curl fehlt)${NC}"; fi
check_port(){ local label="$1" cmd="$2"; printf " • %-5s … " "$label"; timeout 8s bash -lc "$cmd" >/dev/null 2>&1 && ok || fail; }
check_port "25" 'printf "QUIT\r\n" | nc -w 3 127.0.0.1 25'
check_port "465" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:465 -quiet -ign_eof'
check_port "587" 'printf "EHLO x\r\nSTARTTLS\r\nQUIT\r\n" | openssl s_client -starttls smtp -connect 127.0.0.1:587 -quiet -ign_eof'
check_port "110" 'printf "QUIT\r\n" | nc -w 3 127.0.0.1 110'
check_port "995" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:995 -quiet -ign_eof'
check_port "143" 'printf ". LOGOUT\r\n" | nc -w 3 127.0.0.1 143'
check_port "993" 'printf ". LOGOUT\r\n" | openssl s_client -connect 127.0.0.1:993 -quiet -ign_eof'
print_bootstrap_summary "$SERVER_IP" "$ADMIN_USER" "$ADMIN_PASS"
# ===== MOTD =====
install -d /usr/local/bin
cat >/usr/local/bin/mw-motd <<'SH'
#!/usr/bin/env bash
set -euo pipefail
NC="\033[0m"; CY="\033[1;36m"; GR="\033[1;32m"; YE="\033[1;33m"; RD="\033[1;31m"; GY="\033[0;90m"
printf "\033[1;36m"
cat <<'ASCII'
:::: :::: ::: ::::::::::: ::: ::: ::: :::::::: ::: :::::::::::
+:+:+: :+:+:+ :+: :+: :+: :+: :+: :+: :+: :+: :+: :+:
+:+ +:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+
+#+ +:+ +#+ +#++:++#++: +#+ +#+ +#+ +:+ +#+ +#+ +:+ +#+ +#+
+#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+ +#+ +#+ +#+ +#+ +#+
#+# #+# #+# #+# #+# #+# #+#+# #+#+# #+# #+# #+# #+#
### ### ### ### ########### ########## ### ### ######## ########## ###
ASCII
printf "\033[0m\n"
now="$(date '+%Y-%m-%d %H:%M:%S %Z')"
fqdn="$(hostname -f 2>/dev/null || hostname)"
ip_int="$(hostname -I 2>/dev/null | awk '{print $1}')"
ip_ext=""; command -v curl >/dev/null 2>&1 && ip_ext="$(curl -s --max-time 1 https://ifconfig.me || true)"
upt="$(uptime -p 2>/dev/null || true)"
cores="$(nproc 2>/dev/null || echo -n '?')"
mhz="$(LC_ALL=C lscpu 2>/dev/null | awk -F: '/MHz/{gsub(/ /,"",$2); printf("%.0f MHz",$2); exit}')"
[ -z "$mhz" ] && mhz="$(awk -F: '/cpu MHz/{printf("%.0f MHz",$2); exit}' /proc/cpuinfo 2>/dev/null)"
load="$(awk '{print $1" / "$2" / "$3}' /proc/loadavg 2>/dev/null)"
mem_total="$(awk '/MemTotal/{printf "%.2f GB",$2/1024/1024}' /proc/meminfo)"
mem_free="$(awk '/MemAvailable/{printf "%.2f GB",$2/1024/1024}' /proc/meminfo)"
svc_status(){ systemctl is-active --quiet "$1" && echo -e "${GR}OK${NC}" || echo -e "${RD}FAIL${NC}"; }
printf "${CY}Information as of:${NC} ${YE}%s${NC}\n" "$now"
printf "${GY}FQDN :${NC} %s\n" "$fqdn"
if [ -n "$ip_ext" ]; then printf "${GY}IP :${NC} %s ${GY}(external:${NC} %s${GY})${NC}\n" "${ip_int:-?}" "$ip_ext"; else printf "${GY}IP :${NC} %s\n" "${ip_int:-?}"; fi
printf "${GY}Uptime :${NC} %s\n" "${upt:-?}"
printf "${GY}Core(s) :${NC} %s core(s) at ${CY}%s${NC}\n" "$cores" "${mhz:-?}"
printf "${GY}Load :${NC} %s (1 / 5 / 15)\n" "${load:-?}"
printf "${GY}Memory :${NC} ${RD}%s${NC} ${GY}(free)${NC} / ${CY}%s${NC} ${GY}(total)${NC}\n" "${mem_free:-?}" "${mem_total:-?}"
echo
printf "${GY}Services :${NC} postfix: $(svc_status postfix) dovecot: $(svc_status dovecot) nginx: $(svc_status nginx) mariadb: $(svc_status mariadb) redis: $(svc_status redis)\n"
SH
chmod +x /usr/local/bin/mw-motd
if [ -d /etc/update-motd.d ]; then
cat >/etc/update-motd.d/10-mailwolt <<'SH'
#!/usr/bin/env bash
/usr/local/bin/mw-motd
SH
chmod +x /etc/update-motd.d/10-mailwolt
[ -f /etc/update-motd.d/50-motd-news ] && chmod -x /etc/update-motd.d/50-motd-news || true
[ -f /etc/update-motd.d/80-livepatch ] && chmod -x /etc/update-motd.d/80-livepatch || true
else
cat >/etc/profile.d/10-mailwolt-motd.sh <<'SH'
case "$-" in *i*) /usr/local/bin/mw-motd ;; esac
SH
fi
: > /etc/motd 2>/dev/null || true
)