#!/usr/bin/env bash # Первичная настройка нового выделенного сервера (Selectel AR32-NVMe, Ubuntu 24.04). # # Идемпотентен: повторный запуск ничего не ломает и не дублирует. # Ничего не удаляет и не переразмечает диски. # # ВАЖНО: сначала прогнать ops/selectel-audit.sh и убедиться, что диски в зеркале. # Если зеркала нет — ОС переставляется ДО этого скрипта, иначе всю настройку # придётся выкидывать вместе с переустановкой. # # Порядок внутри выбран так, чтобы не потерять доступ: правило для SSH в файрвол # добавляется ДО его включения, парольный вход выключается ПОСЛЕ проверки, что # ключ работает. # # Запуск (от root): ssh root@ 'bash -s' < ops/selectel-bootstrap.sh # # Переменные окружения: # ADMIN_USER — создаваемый пользователь (по умолчанию gendesign) # ADMIN_PUBKEY — публичный ключ для него (обязателен при создании) # SSH_PORT — порт sshd (по умолчанию 22) # SWAP_GB — размер файла подкачки (по умолчанию 8) # TIMEZONE — часовой пояс (по умолчанию Europe/Moscow, как на текущем проде) set -euo pipefail ADMIN_USER="${ADMIN_USER:-gendesign}" ADMIN_PUBKEY="${ADMIN_PUBKEY:-}" SSH_PORT="${SSH_PORT:-22}" SWAP_GB="${SWAP_GB:-8}" # Версии — те же, что на текущем прод-хосте, чтобы поведение compose не разъехалось. WANT_DOCKER="29.4.1" WANT_COMPOSE="v5.1.3" log() { printf '\n\033[1m>>> %s\033[0m\n' "$1"; } warn() { printf '\033[33m[!] %s\033[0m\n' "$1"; } die() { printf '\033[31m[x] %s\033[0m\n' "$1" >&2; exit 1; } [ "$(id -u)" -eq 0 ] || die "запускать от root" log "0. Проверка предусловий" if [ ! -f /proc/mdstat ] || ! grep -q 'raid1' /proc/mdstat 2>/dev/null; then warn "RAID1 в /proc/mdstat не найден." warn "Память на этой платформе non-ECC, диска два — без зеркала отказ одного" warn "диска означает потерю базы. Убедись, что это осознанное решение." warn "Продолжение через 15 секунд, Ctrl-C чтобы прервать." sleep 15 fi log "1. Базовые пакеты" export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq \ ca-certificates curl gnupg git ufw fail2ban unattended-upgrades \ htop ncdu jq nvme-cli smartmontools chrony >/dev/null log "2. Часовой пояс" # Совпадает с текущим прод-хостом (Beget = Europe/Moscow, проверено 22.08.2026). # Ставить UTC нельзя без правки crontab: бэкапы и джобы записаны в местном # времени, смена пояса сдвинула бы все шесть задач на три часа. timedatectl set-timezone "${TIMEZONE:-Europe/Moscow}" systemctl enable --now chrony >/dev/null 2>&1 || true timedatectl | head -3 log "3. Пользователь $ADMIN_USER" if id "$ADMIN_USER" >/dev/null 2>&1; then echo "уже существует" else [ -n "$ADMIN_PUBKEY" ] || die "ADMIN_PUBKEY не задан — иначе доступа к серверу не будет" adduser --disabled-password --gecos "" "$ADMIN_USER" echo "создан" fi if [ -n "$ADMIN_PUBKEY" ]; then install -d -m 700 -o "$ADMIN_USER" -g "$ADMIN_USER" "/home/$ADMIN_USER/.ssh" touch "/home/$ADMIN_USER/.ssh/authorized_keys" grep -qxF "$ADMIN_PUBKEY" "/home/$ADMIN_USER/.ssh/authorized_keys" \ || echo "$ADMIN_PUBKEY" >> "/home/$ADMIN_USER/.ssh/authorized_keys" chmod 600 "/home/$ADMIN_USER/.ssh/authorized_keys" chown -R "$ADMIN_USER:$ADMIN_USER" "/home/$ADMIN_USER/.ssh" echo "ключ на месте" fi usermod -aG sudo "$ADMIN_USER" echo "$ADMIN_USER ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/90-$ADMIN_USER" chmod 440 "/etc/sudoers.d/90-$ADMIN_USER" log "4. Файрвол (правило для SSH — до включения)" ufw --force reset >/dev/null ufw default deny incoming >/dev/null ufw default allow outgoing >/dev/null ufw allow "$SSH_PORT/tcp" comment 'ssh' >/dev/null ufw allow 80/tcp comment 'http' >/dev/null ufw allow 443/tcp comment 'https' >/dev/null ufw --force enable >/dev/null ufw status verbose log "5. Docker $WANT_DOCKER + Compose $WANT_COMPOSE" if ! command -v docker >/dev/null 2>&1; then install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/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/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ > /etc/apt/sources.list.d/docker.list apt-get update -qq apt-get install -y -qq docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin >/dev/null fi usermod -aG docker "$ADMIN_USER" systemctl enable --now docker >/dev/null have_docker="$(docker --version | grep -oP '\d+\.\d+\.\d+' | head -1)" have_compose="$(docker compose version --short 2>/dev/null || echo '?')" echo "docker $have_docker · compose $have_compose" [ "$have_docker" = "$WANT_DOCKER" ] || warn "docker $have_docker != $WANT_DOCKER на текущем проде" case "$have_compose" in "${WANT_COMPOSE#v}"*) ;; *) warn "compose $have_compose != $WANT_COMPOSE" ;; esac log "6. Журналы Docker — чтобы диск не съело логами" if [ ! -f /etc/docker/daemon.json ]; then install -d /etc/docker cat > /etc/docker/daemon.json <<'JSON' { "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" }, "live-restore": true } JSON systemctl restart docker echo "daemon.json создан" else echo "daemon.json уже есть — не трогаю" fi log "7. Swap ${SWAP_GB}G" # Установщик Selectel уже отдаёт swap-раздел (на AR32-NVMe — 4.7G в зеркале md). # Добавлять файл поверх раздела незачем: памяти 62G, своп тут страховка. if swapon --show --noheadings 2>/dev/null | grep -q .; then echo "swap уже есть, файл не создаю:" swapon --show elif swapon --show | grep -q '/swapfile'; then echo "уже подключён" else fallocate -l "${SWAP_GB}G" /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=$((SWAP_GB*1024)) chmod 600 /swapfile mkswap /swapfile >/dev/null swapon /swapfile grep -q '^/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab echo "создан" fi cat > /etc/sysctl.d/60-gendesign.conf <<'SYSCTL' # Памяти 64 ГБ — своп только страховка, в него лезть не надо. vm.swappiness = 10 # Redis просит overcommit, иначе фоновое сохранение может не запуститься. vm.overcommit_memory = 1 # Больше открытых файлов: два десятка контейнеров + Postgres. fs.file-max = 2097152 SYSCTL sysctl --system >/dev/null sysctl vm.swappiness vm.overcommit_memory log "8. Автообновления безопасности" cat > /etc/apt/apt.conf.d/20auto-upgrades <<'APT' APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1"; APT systemctl enable --now unattended-upgrades >/dev/null 2>&1 || true log "9. fail2ban на sshd" cat > /etc/fail2ban/jail.d/sshd.local </dev/null systemctl is-active fail2ban log "10. Ужесточение sshd" cat > /etc/ssh/sshd_config.d/60-hardening.conf <