From b49ef93619937975c02f69ef29607209eaa89a5e Mon Sep 17 00:00:00 2001 From: Vladislav Kopnin Date: Sat, 10 Oct 2026 11:51:14 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D0=BD=20install.sh:=20?= =?UTF-8?q?=D1=83=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BA=D0=B0=20=D1=87?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B7=20deploy.sh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Для личного использования достаточно deploy.sh (проверяет окружение, собирает образы и поднимает контейнеры). Раздел «Установка на сервер» переписан: Docker через deploy.sh + вариант без Docker (systemd) и nginx, без интерактивного установщика и его флагов. --- install.sh | 324 ----------------------------------------------------- 1 file changed, 324 deletions(-) delete mode 100755 install.sh diff --git a/install.sh b/install.sh deleted file mode 100755 index b6c5f9f..0000000 --- a/install.sh +++ /dev/null @@ -1,324 +0,0 @@ -#!/usr/bin/env bash -# Интерактивная установка «Расписания» на чистый сервер. -# -# Запуск из распакованного каталога проекта (нужен root — скрипт сам -# перезапустится через sudo): -# -# bash install.sh -# -# Скрипт спросит: как ставить (Docker или прямо в систему), публичную ссылку -# на таблицу и домен сайта. Затем поставит всё нужное, запустит сервисы и -# настроит nginx с сертификатом Let's Encrypt. -# -# Для автоматизации ответы можно передать аргументами: -# --mode docker|system способ установки -# --link URL публичная ссылка Яндекс.Диска с таблицей -# --domain DOMAIN домен сайта (пусто = без nginx, сайт на 127.0.0.1) -# --api-port N порт api (по умолчанию 8000) -# --front-port N порт сайта (по умолчанию 8080) -# --user NAME пользователь служб (режим «в систему») -# --email ADDR почта для уведомлений Let's Encrypt -# --dry-run показать план установки, ничего не меняя -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; } -err() { printf '\033[31m==>\033[0m %s\n' "$*" >&2; } -die() { err "$*"; exit 1; } - -usage() { grep '^#' "$0" | sed 's/^# \{0,1\}//' | tail -n +2; } - -MODE="" LINK="" DOMAIN="" DOMAIN_SET=0 API_PORT="" FRONT_PORT="" RUN_USER="" EMAIL="" DRY_RUN=0 - -while [[ $# -gt 0 ]]; do - case "$1" in - --mode) MODE="$2"; shift 2 ;; - --link) LINK="$2"; shift 2 ;; - --domain) DOMAIN="$2"; DOMAIN_SET=1; shift 2 ;; - --api-port) API_PORT="$2"; shift 2 ;; - --front-port) FRONT_PORT="$2"; shift 2 ;; - --user) RUN_USER="$2"; shift 2 ;; - --email) EMAIL="$2"; shift 2 ;; - --dry-run) DRY_RUN=1; shift ;; - -h|--help) usage; exit 0 ;; - *) die "Неизвестный аргумент: $1 (см. --help)" ;; - esac -done - -# ask "Вопрос" [по_умолчанию] — ответ в $REPLY -ask() { - local def="${2:-}" input - if ! read -rp "$1${def:+ [$def]}: " input; then - die "Нужен интерактивный ввод — запустите в терминале или передайте все параметры флагами (см. --help)" - fi - REPLY="${input:-$def}" -} - -# --- вопросы ------------------------------------------------------------------- - -if [[ -z $MODE ]]; then - log "Как ставить?" - log " 1) Docker — изолированные контейнеры, автоперезапуск, обновление одной командой (рекомендуется)" - log " 2) Система — без Docker: venv + службы systemd" - while true; do - ask "Способ установки (docker/system)" "docker" - REPLY=$(echo "$REPLY" | tr '[:upper:]' '[:lower:]' | xargs) - if [[ $REPLY == 1 ]]; then - REPLY=docker - elif [[ $REPLY == 2 ]]; then - REPLY=system - fi - [[ $REPLY =~ ^(docker|system)$ ]] && break || err "Ответьте docker или system" - done - MODE=$REPLY -fi -[[ $MODE =~ ^(docker|system)$ ]] || die "--mode: только docker или system" - -if [[ -z $LINK ]]; then - while true; do - ask "Публичная ссылка на таблицу (Яндекс.Диск)" - REPLY=$(echo "$REPLY" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr -d '"') - [[ -n $REPLY ]] && break || err "Без ссылки приложение не сможет скачать расписание" - done - LINK=$REPLY -fi - -if [[ $DOMAIN_SET == 0 && -z $DOMAIN ]]; then - ask "Домен сайта, например sched.example.ru (пусто = без nginx, сайт только на 127.0.0.1)" - DOMAIN=$(echo "$REPLY" | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') -fi -if [[ -n $DOMAIN && ! $DOMAIN =~ ^[a-z0-9.-]+$ ]]; then die "Кривое имя домена: $DOMAIN"; fi - -if [[ -z $API_PORT ]]; then - ask "Порт api (нужен только для диагностики)" "8000" - API_PORT=$REPLY -fi -if [[ -z $FRONT_PORT ]]; then - ask "Порт сайта" "8080" - FRONT_PORT=$REPLY -fi -[[ $API_PORT =~ ^[0-9]+$ && $FRONT_PORT =~ ^[0-9]+$ ]] || die "Порты должны быть числами" - -if [[ $MODE == system ]]; then - if [[ -z $RUN_USER ]]; then - ask "Пользователь, от которого будут работать службы" "${SUDO_USER:-$(id -un)}" - RUN_USER=$REPLY - fi - id "$RUN_USER" >/dev/null 2>&1 || die "Пользователь $RUN_USER не существует" -fi -if [[ -n $DOMAIN && -z $EMAIL ]]; then - ask "Почта для уведомлений Let's Encrypt" "admin@$DOMAIN" - EMAIL=$REPLY -fi - -if [[ $DRY_RUN == 1 ]]; then - log "План установки (dry-run, ничего не выполнено):" - echo " режим: $MODE" - echo " таблица: $LINK" - echo " домен: ${DOMAIN:-(не задан — nginx и https пропускаются, сайт на 127.0.0.1)}" - echo " порты: api $API_PORT, сайт $FRONT_PORT" - if [[ $MODE == system ]]; then - echo " службы от: $RUN_USER" - fi - if [[ -n $DOMAIN ]]; then - echo " сертификат: Let's Encrypt для $DOMAIN (почта: $EMAIL)" - fi - exit 0 -fi - -# --- права ---------------------------------------------------------------------- - -if [[ $EUID -ne 0 ]]; then - if [[ ${INSTALL_NO_SUDO:-0} == 1 ]]; then - err "Запущено без root (INSTALL_NO_SUDO=1) — часть шагов может не пройти" - else - log "Нужны права root — перезапускаюсь через sudo" - exec sudo bash "$0" "$@" - fi -fi - -# --- помощники ------------------------------------------------------------------ - -pkg_install() { - local missing=() p - for p in "$@"; do command -v "$p" >/dev/null 2>&1 || missing+=("$p"); done - if [[ ${#missing[@]} -eq 0 ]]; then - return 0 - fi - log "Ставлю пакеты: ${missing[*]}" - if command -v apt-get >/dev/null 2>&1; then - apt-get update -y - DEBIAN_FRONTEND=noninteractive apt-get install -y "${missing[@]}" - elif command -v dnf >/dev/null 2>&1; then - dnf install -y "${missing[@]}" - else - die "Не нашёл apt-get/dnf — поставьте вручную: ${missing[*]}" - fi -} - -wait_health() { - local url="$1" name="$2" i - printf '%s' "==> Жду $name ($url)" - for i in $(seq 1 60); do - if curl -sf -m 2 "$url" >/dev/null 2>&1; then echo " — готово"; return 0; fi - printf '.'; sleep 1 - done - echo - die "$name не отвечает на $url — смотрите логи (docker logs / journalctl)" -} - -write_env() { - cd "$ROOT" - if [[ -f .env && ! -f .env.install-backup ]]; then - cp .env .env.install-backup - fi - cat > .env < -ADMIN_PATH= -ADMIN_LOGIN= -ADMIN_PASSWORD= -ADMIN_MAX_ATTEMPTS=3 -ADMIN_LOCKOUT_MINUTES=15 -EOF - log ".env записан (прежний, если был, сохранён в .env.install-backup)" -} - -install_docker() { - if command -v docker >/dev/null 2>&1; then - log "Docker уже установлен" - else - log "Ставлю Docker..." - curl -fsSL https://get.docker.com | sh - fi - if [[ $EUID -eq 0 ]]; then - systemctl enable --now docker - fi -} - -setup_nginx() { - pkg_install nginx certbot curl - if [[ ! -d /etc/nginx/sites-available ]]; then - die "Нужен nginx с поддержкой sites-available (Debian/Ubuntu); на других системах настройте конфиг по README" - fi - log "Временный http-конфиг $DOMAIN — для выпуска сертификата" - cat > "/etc/nginx/sites-available/$DOMAIN" <<'NGINX' -# Сгенерировано install.sh: временный конфиг для certbot, потом заменяется -server { - listen 80; - listen [::]:80; - server_name __DOMAIN__; - root /var/www/html; - location /.well-known/acme-challenge/ { } -} -NGINX - sed -i "s/__DOMAIN__/$DOMAIN/" "/etc/nginx/sites-available/$DOMAIN" - ln -sf "/etc/nginx/sites-available/$DOMAIN" "/etc/nginx/sites-enabled/$DOMAIN" - nginx -t && systemctl reload nginx - - log "Выпускаю сертификат Let's Encrypt для $DOMAIN (webroot, без остановки nginx)" - certbot certonly --webroot -w /var/www/html -d "$DOMAIN" \ - --non-interactive --agree-tos -m "$EMAIL" - - log "Финальный конфиг сайта: https + прокси на 127.0.0.1:$FRONT_PORT" - "$ROOT/deploy/nginx-site.sh" --domain "$DOMAIN" --cert "$DOMAIN" --port "$FRONT_PORT" -} - -# --- установка ------------------------------------------------------------------ - -if [[ $MODE == docker ]]; then - install_docker - write_env - log "Собираю и запускаю контейнеры (первая сборка — несколько минут)..." - (cd "$ROOT" && docker compose up -d --build) - wait_health "http://127.0.0.1:$FRONT_PORT/healthz" "frontend" -else - log "Проверяю Python..." - pkg_install python3 python3-pip - if command -v apt-get >/dev/null 2>&1; then - apt-get install -y python3-venv # на Debian/Ubuntu venv без этого пакета не работает - fi - python3 - <<'PY' || die "Нужен Python 3.11 или новее (см. README, «Зависимости»)" -import sys -sys.exit(0 if sys.version_info >= (3, 11) else 1) -PY - write_env - log "Создаю виртуальное окружение и ставлю зависимости..." - [[ -d "$ROOT/.venv" ]] || python3 -m venv "$ROOT/.venv" - "$ROOT/.venv/bin/pip" install -q --upgrade pip - "$ROOT/.venv/bin/pip" install -q -r "$ROOT/requirements.txt" - - log "Настраиваю службы systemd..." - cat > /etc/systemd/system/schedule-api.service < /etc/systemd/system/schedule-frontend.service <