#!/usr/bin/env bash # Mail-stack installer. # # Idempotent — можно запускать повторно, шаги уже выполненные пропускаются. # Что делает: # 1. Проверяет/ставит системные зависимости (docker, python3, build-essential) # 2. Создаёт venv и ставит requirements.txt # 3. Готовит .env (копирует из .env.example если нет) # 4. Поднимает docker-compose стек (postgres, redis, meili, docker-mailserver) # 5. Дожидается БД и применяет миграции Alembic # 6. Устанавливает systemd-юниты (mail-flask, mail-dramatiq) и поднимает их # 7. Прописывает cron на ежедневный backup # 8. Применяет fail2ban override для DMS (whitelist Docker bridge) # # Запуск: # sudo bash deploy/install.sh # # Переменные окружения (опционально): # PROJECT_DIR=/home/deeily/mail — куда установлено приложение # APP_USER=deeily — под кем работает Flask # SKIP_DOCKER=1 — пропустить установку пакетов и compose # SKIP_SYSTEMD=1 — пропустить установку systemd-юнитов # SKIP_CRON=1 — пропустить cron-запись для бэкапа set -euo pipefail # ── Параметры ──────────────────────────────────────────────────────────── HERE="$(cd "$(dirname "$0")" && pwd)" ROOT="$(cd "$HERE/.." && pwd)" APP_USER="${APP_USER:-${SUDO_USER:-$USER}}" PROJECT_DIR="${PROJECT_DIR:-$ROOT}" PYTHON_BIN="${PYTHON_BIN:-python3}" note() { echo -e "\033[1;34m==>\033[0m $*"; } warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; } err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } need_root() { [ "$(id -u)" -eq 0 ] || { err "запусти под root: sudo bash $0"; exit 1; }; } # ── 0. Sanity ─────────────────────────────────────────────────────────── need_root note "user=$APP_USER dir=$PROJECT_DIR" [ -d "$PROJECT_DIR" ] || { err "PROJECT_DIR не существует: $PROJECT_DIR"; exit 1; } # ── 1. Системные пакеты ───────────────────────────────────────────────── if [ "${SKIP_DOCKER:-0}" != "1" ]; then if ! command -v docker >/dev/null; then note "ставлю docker / docker-compose-plugin" apt-get update -y apt-get install -y ca-certificates curl gnupg install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" \ > /etc/apt/sources.list.d/docker.list apt-get update -y apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin usermod -aG docker "$APP_USER" || true fi if ! command -v "$PYTHON_BIN" >/dev/null; then apt-get install -y python3 python3-venv python3-pip build-essential libpq-dev fi fi # ── 2. venv + requirements ────────────────────────────────────────────── note "venv + pip install" sudo -u "$APP_USER" -- bash -c " cd '$PROJECT_DIR' [ -d .venv ] || $PYTHON_BIN -m venv .venv ./.venv/bin/pip install --quiet --upgrade pip ./.venv/bin/pip install --quiet -r requirements.txt " # ── 3. .env ───────────────────────────────────────────────────────────── if [ ! -f "$PROJECT_DIR/.env" ]; then note "создаю .env из .env.example" cp "$PROJECT_DIR/.env.example" "$PROJECT_DIR/.env" SECRET=$(head -c 32 /dev/urandom | base64 | tr -d '/+=' | head -c 40) sed -i "s|^FLASK_SECRET_KEY=.*|FLASK_SECRET_KEY=$SECRET|" "$PROJECT_DIR/.env" chown "$APP_USER:$APP_USER" "$PROJECT_DIR/.env" chmod 600 "$PROJECT_DIR/.env" warn ".env создан. Открой и заполни ADMINS, IMAP_PASSWORD, MAIL_SERVER_HOSTNAME." fi # ── 4. docker-compose стек ────────────────────────────────────────────── if [ "${SKIP_DOCKER:-0}" != "1" ]; then # Копируем безопасные конфиги DMS из репы в docker/dms-config/ (он в .gitignore). mkdir -p "$PROJECT_DIR/docker/dms-config" for f in fail2ban-jail.cf postfix-main.cf; do if [ -f "$PROJECT_DIR/deploy/dms-config/$f" ] && [ ! -f "$PROJECT_DIR/docker/dms-config/$f" ]; then cp "$PROJECT_DIR/deploy/dms-config/$f" "$PROJECT_DIR/docker/dms-config/$f" fi done chown -R "$APP_USER:$APP_USER" "$PROJECT_DIR/docker/dms-config" note "поднимаю docker-compose (postgres, redis, meili, mailserver)" sudo -u "$APP_USER" -- bash -c "cd '$PROJECT_DIR/docker' && docker compose up -d" # Fail2ban override для DMS — whitelist Docker bridge. if [ -f "$PROJECT_DIR/docker/dms-config/fail2ban-jail.cf" ]; then docker exec docker-mailserver-1 cp /tmp/docker-mailserver/fail2ban-jail.cf /etc/fail2ban/jail.d/user-jail.local 2>/dev/null || true docker exec docker-mailserver-1 supervisorctl restart fail2ban 2>/dev/null || true fi fi # ── 5. Postgres миграции ──────────────────────────────────────────────── note "жду postgres + alembic upgrade head" for i in {1..30}; do if docker exec docker-postgres-1 pg_isready -U mail >/dev/null 2>&1; then break; fi sleep 1 done sudo -u "$APP_USER" -- bash -c "cd '$PROJECT_DIR' && ./.venv/bin/alembic upgrade head" # ── 6. systemd ────────────────────────────────────────────────────────── if [ "${SKIP_SYSTEMD:-0}" != "1" ]; then note "ставлю systemd-юниты" bash "$HERE/systemd/install.sh" fi # ── 7. cron для бэкапа ────────────────────────────────────────────────── if [ "${SKIP_CRON:-0}" != "1" ]; then note "прописываю ежедневный backup в crontab @ 03:30" touch /var/log/mail-backup.log chown "$APP_USER:$APP_USER" /var/log/mail-backup.log CRON_LINE="30 3 * * * $PROJECT_DIR/deploy/backup.sh >>/var/log/mail-backup.log 2>&1" EXISTING=$(sudo -u "$APP_USER" crontab -l 2>/dev/null | grep -v "$PROJECT_DIR/deploy/backup.sh" || true) printf '%s\n%s\n' "$EXISTING" "$CRON_LINE" | sudo -u "$APP_USER" crontab - fi # ── 8. Sudoers для ELK-apply через UI ──────────────────────────────────── if [ ! -f /etc/sudoers.d/mail-elk ]; then note "ставлю sudoers entry для UI-кнопки 'Применить ELK'" cat > /etc/sudoers.d/mail-elk <