Template
575 lines
17 KiB
Bash
575 lines
17 KiB
Bash
#!/usr/bin/env bash
|
|
# VPN Service Bot — интерактивный установщик (Docker + Postgres + Caddy SSL)
|
|
# Запуск на Ubuntu 22.04 / 24.04:
|
|
# cd /path/to/project && sudo bash install.sh
|
|
#
|
|
# Если видишь ошибку вроде «set: pipefail» / «invalid option name» —
|
|
# файл сохранён с Windows-переносами (CRLF). Исправь так:
|
|
# sed -i 's/\r$//' install.sh && sudo bash install.sh
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
cd "$SCRIPT_DIR"
|
|
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
CYAN='\033[0;36m'
|
|
BOLD='\033[1m'
|
|
NC='\033[0m'
|
|
|
|
log() { echo -e "${GREEN}✓${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}!${NC} $*"; }
|
|
err() { echo -e "${RED}✗${NC} $*" >&2; }
|
|
info() { echo -e "${CYAN}→${NC} $*"; }
|
|
title() {
|
|
echo
|
|
echo -e "${BOLD}════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} $*${NC}"
|
|
echo -e "${BOLD}════════════════════════════════════════${NC}"
|
|
}
|
|
|
|
need_root() {
|
|
if [[ "${EUID}" -ne 0 ]]; then
|
|
err "Запусти от root: sudo bash install.sh"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
ask() {
|
|
# ask "Prompt" "default" → sets REPLY
|
|
local prompt="$1"
|
|
local default="${2:-}"
|
|
if [[ -n "$default" ]]; then
|
|
read -r -p "$(echo -e "${CYAN}?${NC} ${prompt} [${default}]: ")" REPLY
|
|
REPLY="${REPLY:-$default}"
|
|
else
|
|
read -r -p "$(echo -e "${CYAN}?${NC} ${prompt}: ")" REPLY
|
|
fi
|
|
}
|
|
|
|
ask_secret() {
|
|
local prompt="$1"
|
|
local default="${2:-}"
|
|
if [[ -n "$default" ]]; then
|
|
read -r -s -p "$(echo -e "${CYAN}?${NC} ${prompt} [скрыто / Enter = сгенерировать]: ")" REPLY
|
|
echo
|
|
REPLY="${REPLY:-$default}"
|
|
else
|
|
read -r -s -p "$(echo -e "${CYAN}?${NC} ${prompt}: ")" REPLY
|
|
echo
|
|
fi
|
|
}
|
|
|
|
ask_yn() {
|
|
local prompt="$1"
|
|
local default="${2:-y}"
|
|
local hint="y/n"
|
|
[[ "$default" == "y" ]] && hint="Y/n"
|
|
[[ "$default" == "n" ]] && hint="y/N"
|
|
while true; do
|
|
read -r -p "$(echo -e "${CYAN}?${NC} ${prompt} [${hint}]: ")" REPLY
|
|
REPLY="${REPLY:-$default}"
|
|
case "${REPLY,,}" in
|
|
y|yes|д|да) REPLY=y; return 0 ;;
|
|
n|no|н|нет) REPLY=n; return 0 ;;
|
|
esac
|
|
warn "Ответь y или n"
|
|
done
|
|
}
|
|
|
|
rand_pass() {
|
|
if command -v openssl >/dev/null 2>&1; then
|
|
openssl rand -base64 24 | tr -d '/+=' | head -c 24
|
|
else
|
|
head -c 48 /dev/urandom | base64 | tr -d '/+=' | head -c 24
|
|
fi
|
|
}
|
|
|
|
env_escape() {
|
|
# Значение для .env: кавычки если есть спецсимволы
|
|
local v="$1"
|
|
if [[ -z "$v" ]]; then
|
|
echo ""
|
|
return
|
|
fi
|
|
if [[ "$v" =~ [[:space:]\#\"\'\\\$\`] ]]; then
|
|
v="${v//\\/\\\\}"
|
|
v="${v//\"/\\\"}"
|
|
echo "\"$v\""
|
|
else
|
|
echo "$v"
|
|
fi
|
|
}
|
|
|
|
check_os() {
|
|
if [[ ! -f /etc/os-release ]]; then
|
|
warn "Не Linux? Скрипт рассчитан на Ubuntu 22.04/24.04"
|
|
return
|
|
fi
|
|
# shellcheck source=/dev/null
|
|
. /etc/os-release
|
|
info "ОС: ${PRETTY_NAME:-unknown}"
|
|
if [[ "${ID:-}" != "ubuntu" && "${ID:-}" != "debian" ]]; then
|
|
warn "Рекомендуется Ubuntu/Debian. Продолжаем на свой страх."
|
|
fi
|
|
}
|
|
|
|
install_docker() {
|
|
title "Docker"
|
|
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
|
log "Docker уже установлен: $(docker --version)"
|
|
log "Compose: $(docker compose version)"
|
|
return
|
|
fi
|
|
|
|
ask_yn "Docker не найден. Установить Docker Engine + Compose?" "y"
|
|
if [[ "$REPLY" != "y" ]]; then
|
|
err "Без Docker установка невозможна"
|
|
exit 1
|
|
fi
|
|
|
|
info "Установка Docker…"
|
|
apt-get update -y
|
|
apt-get install -y ca-certificates curl gnupg
|
|
install -m 0755 -d /etc/apt/keyrings
|
|
if [[ ! -f /etc/apt/keyrings/docker.asc ]]; then
|
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
|
chmod a+r /etc/apt/keyrings/docker.asc
|
|
fi
|
|
|
|
local codename
|
|
# shellcheck source=/dev/null
|
|
. /etc/os-release
|
|
codename="${VERSION_CODENAME:-jammy}"
|
|
if [[ "${ID:-}" == "debian" ]]; then
|
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian ${codename} stable" \
|
|
> /etc/apt/sources.list.d/docker.list
|
|
else
|
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu ${codename} stable" \
|
|
> /etc/apt/sources.list.d/docker.list
|
|
fi
|
|
|
|
apt-get update -y
|
|
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
|
systemctl enable --now docker
|
|
log "Docker установлен"
|
|
}
|
|
|
|
check_ports() {
|
|
title "Порты 80 / 443 (нужны для Caddy SSL)"
|
|
local busy=0
|
|
for port in 80 443; do
|
|
if ss -tlnp 2>/dev/null | grep -qE ":${port}\\b"; then
|
|
warn "Порт ${port} занят:"
|
|
ss -tlnp 2>/dev/null | grep -E ":${port}\\b" || true
|
|
busy=1
|
|
else
|
|
log "Порт ${port} свободен"
|
|
fi
|
|
done
|
|
if [[ "$busy" -eq 1 ]]; then
|
|
ask_yn "Порты заняты. Остановить конфликтующие сервисы (nginx/apache2/caddy)?" "n"
|
|
if [[ "$REPLY" == "y" ]]; then
|
|
systemctl stop nginx 2>/dev/null || true
|
|
systemctl disable nginx 2>/dev/null || true
|
|
systemctl stop apache2 2>/dev/null || true
|
|
systemctl disable apache2 2>/dev/null || true
|
|
systemctl stop caddy 2>/dev/null || true
|
|
# чужой docker caddy
|
|
docker ps --format '{{.Names}}' 2>/dev/null | while read -r name; do
|
|
if docker port "$name" 2>/dev/null | grep -qE ':80|:443'; then
|
|
warn "Останавливаю контейнер $name (держит 80/443)"
|
|
docker stop "$name" || true
|
|
fi
|
|
done
|
|
else
|
|
warn "Caddy может не получить SSL, пока 80/443 заняты"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
configure_firewall() {
|
|
title "Firewall"
|
|
if ! command -v ufw >/dev/null 2>&1; then
|
|
ask_yn "UFW не установлен. Поставить и открыть 22/80/443?" "y"
|
|
if [[ "$REPLY" == "y" ]]; then
|
|
apt-get install -y ufw
|
|
else
|
|
return
|
|
fi
|
|
fi
|
|
ask_yn "Настроить UFW: разрешить 22, 80, 443?" "y"
|
|
if [[ "$REPLY" != "y" ]]; then
|
|
return
|
|
fi
|
|
ufw allow OpenSSH >/dev/null 2>&1 || ufw allow 22/tcp >/dev/null 2>&1 || true
|
|
ufw allow 80/tcp >/dev/null 2>&1 || true
|
|
ufw allow 443/tcp >/dev/null 2>&1 || true
|
|
ufw allow 443/udp >/dev/null 2>&1 || true
|
|
echo "y" | ufw enable >/dev/null 2>&1 || true
|
|
log "UFW: 22 / 80 / 443 открыты"
|
|
}
|
|
|
|
collect_config() {
|
|
title "Настройки проекта"
|
|
|
|
ask "Домен Mini App (A-запись → IP этого сервера)" "bot.example.com"
|
|
WEBAPP_DOMAIN="${REPLY,,}"
|
|
WEBAPP_DOMAIN="${WEBAPP_DOMAIN#https://}"
|
|
WEBAPP_DOMAIN="${WEBAPP_DOMAIN#http://}"
|
|
WEBAPP_DOMAIN="${WEBAPP_DOMAIN%%/*}"
|
|
WEBAPP_URL="https://${WEBAPP_DOMAIN}"
|
|
|
|
ask "Email для Let's Encrypt (Caddy, необязательно)" ""
|
|
ACME_EMAIL="$REPLY"
|
|
|
|
echo
|
|
info "Remnawave панель"
|
|
ask "REMNAWAVE_BASE_URL" "https://panel.example.com"
|
|
REMNAWAVE_BASE_URL="${REPLY%/}"
|
|
|
|
ask_yn "Использовать API Token панели? (иначе логин/пароль)" "y"
|
|
if [[ "$REPLY" == "y" ]]; then
|
|
ask_secret "REMNAWAVE_TOKEN"
|
|
REMNAWAVE_TOKEN="$REPLY"
|
|
REMNAWAVE_USERNAME=""
|
|
REMNAWAVE_PASSWORD=""
|
|
while [[ -z "$REMNAWAVE_TOKEN" ]]; do
|
|
warn "Токен обязателен"
|
|
ask_secret "REMNAWAVE_TOKEN"
|
|
REMNAWAVE_TOKEN="$REPLY"
|
|
done
|
|
else
|
|
REMNAWAVE_TOKEN=""
|
|
ask "REMNAWAVE_USERNAME" "admin"
|
|
REMNAWAVE_USERNAME="$REPLY"
|
|
ask_secret "REMNAWAVE_PASSWORD"
|
|
REMNAWAVE_PASSWORD="$REPLY"
|
|
fi
|
|
|
|
echo
|
|
info "Telegram бот (@BotFather)"
|
|
ask_secret "BOT_TOKEN"
|
|
BOT_TOKEN="$REPLY"
|
|
while [[ -z "$BOT_TOKEN" ]]; do
|
|
warn "BOT_TOKEN обязателен"
|
|
ask_secret "BOT_TOKEN"
|
|
BOT_TOKEN="$REPLY"
|
|
done
|
|
|
|
ask "ADMIN_IDS (Telegram ID через запятую)" ""
|
|
ADMIN_IDS="$REPLY"
|
|
|
|
echo
|
|
info "PostgreSQL (контейнер в Docker Compose)"
|
|
ask "POSTGRES_USER" "vpnbot"
|
|
POSTGRES_USER="$REPLY"
|
|
GEN_PG="$(rand_pass)"
|
|
ask_secret "POSTGRES_PASSWORD (Enter = сгенерировать)" "$GEN_PG"
|
|
POSTGRES_PASSWORD="$REPLY"
|
|
ask "POSTGRES_DB" "vpnbot"
|
|
POSTGRES_DB="$REPLY"
|
|
|
|
echo
|
|
info "Веб-админка https://${WEBAPP_DOMAIN}/admin"
|
|
ask "WEB_ADMIN_LOGIN" "admin"
|
|
WEB_ADMIN_LOGIN="$REPLY"
|
|
GEN_ADM="$(rand_pass)"
|
|
ask_secret "WEB_ADMIN_PASSWORD (Enter = сгенерировать)" "$GEN_ADM"
|
|
WEB_ADMIN_PASSWORD="$REPLY"
|
|
|
|
echo
|
|
ask_yn "Включить Telegram Stars для пополнения?" "y"
|
|
if [[ "$REPLY" == "y" ]]; then
|
|
STARS_ENABLED="true"
|
|
ask "Курс ₽ за 1⭐" "1.5"
|
|
STARS_RUB_PER_STAR="$REPLY"
|
|
else
|
|
STARS_ENABLED="false"
|
|
STARS_RUB_PER_STAR="1.5"
|
|
fi
|
|
|
|
ask "Ссылка покупки кода пополнения (TOPUP_CODE_PAY_URL, можно пусто)" ""
|
|
TOPUP_CODE_PAY_URL="$REPLY"
|
|
|
|
echo
|
|
ask_yn "Настроить SMTP сейчас? (можно позже в /admin → SMTP)" "n"
|
|
if [[ "$REPLY" == "y" ]]; then
|
|
ask "SMTP_HOST" ""
|
|
SMTP_HOST="$REPLY"
|
|
ask "SMTP_PORT" "587"
|
|
SMTP_PORT="$REPLY"
|
|
ask "SMTP_USER" ""
|
|
SMTP_USER="$REPLY"
|
|
ask_secret "SMTP_PASSWORD"
|
|
SMTP_PASSWORD="$REPLY"
|
|
ask "SMTP_FROM" ""
|
|
SMTP_FROM="$REPLY"
|
|
ask_yn "SMTP STARTTLS?" "y"
|
|
[[ "$REPLY" == "y" ]] && SMTP_TLS="true" || SMTP_TLS="false"
|
|
ask_yn "SMTP SSL (порт 465)?" "n"
|
|
[[ "$REPLY" == "y" ]] && SMTP_SSL="true" || SMTP_SSL="false"
|
|
else
|
|
SMTP_HOST=""
|
|
SMTP_PORT="587"
|
|
SMTP_USER=""
|
|
SMTP_PASSWORD=""
|
|
SMTP_FROM=""
|
|
SMTP_TLS="true"
|
|
SMTP_SSL="false"
|
|
fi
|
|
|
|
echo
|
|
warn "Платёжные ключи (Crypto/Lava/Heleket/…) можно заполнить позже в веб-админке."
|
|
ask_yn "Продолжить установку?" "y"
|
|
if [[ "$REPLY" != "y" ]]; then
|
|
err "Отменено"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
write_env() {
|
|
title "Запись .env"
|
|
local env_file="${SCRIPT_DIR}/.env"
|
|
if [[ -f "$env_file" ]]; then
|
|
local bak="${env_file}.bak.$(date +%Y%m%d-%H%M%S)"
|
|
cp -a "$env_file" "$bak"
|
|
warn "Старый .env сохранён: $bak"
|
|
fi
|
|
|
|
cat > "$env_file" <<EOF
|
|
# Сгенерировано install.sh $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
|
|
REMNAWAVE_BASE_URL=$(env_escape "$REMNAWAVE_BASE_URL")
|
|
REMNAWAVE_TOKEN=$(env_escape "$REMNAWAVE_TOKEN")
|
|
REMNAWAVE_USERNAME=$(env_escape "$REMNAWAVE_USERNAME")
|
|
REMNAWAVE_PASSWORD=$(env_escape "$REMNAWAVE_PASSWORD")
|
|
|
|
BOT_TOKEN=$(env_escape "$BOT_TOKEN")
|
|
ADMIN_IDS=$(env_escape "$ADMIN_IDS")
|
|
|
|
STARS_ENABLED=${STARS_ENABLED}
|
|
STARS_RUB_PER_STAR=${STARS_RUB_PER_STAR}
|
|
|
|
POSTGRES_USER=$(env_escape "$POSTGRES_USER")
|
|
POSTGRES_PASSWORD=$(env_escape "$POSTGRES_PASSWORD")
|
|
POSTGRES_DB=$(env_escape "$POSTGRES_DB")
|
|
|
|
WEBAPP_URL=$(env_escape "$WEBAPP_URL")
|
|
WEBAPP_DOMAIN=$(env_escape "$WEBAPP_DOMAIN")
|
|
WEBAPP_HOST=0.0.0.0
|
|
WEBAPP_PORT=8080
|
|
|
|
WEB_ADMIN_LOGIN=$(env_escape "$WEB_ADMIN_LOGIN")
|
|
WEB_ADMIN_PASSWORD=$(env_escape "$WEB_ADMIN_PASSWORD")
|
|
|
|
TOPUP_CODE_PAY_URL=$(env_escape "$TOPUP_CODE_PAY_URL")
|
|
|
|
CRYPTO_BOT_TOKEN=
|
|
CRYPTO_PAY_TESTNET=false
|
|
HELEKET_MERCHANT_ID=
|
|
HELEKET_API_KEY=
|
|
LAVA_SHOP_ID=
|
|
LAVA_SECRET_KEY=
|
|
LAVA_ADDITIONAL_KEY=
|
|
ANYPAY_API_ID=
|
|
ANYPAY_API_KEY=
|
|
ANYPAY_PROJECT_ID=
|
|
DIGISELLER_SELLER_ID=
|
|
DIGISELLER_API_KEY=
|
|
|
|
SMTP_HOST=$(env_escape "$SMTP_HOST")
|
|
SMTP_PORT=${SMTP_PORT}
|
|
SMTP_USER=$(env_escape "$SMTP_USER")
|
|
SMTP_PASSWORD=$(env_escape "$SMTP_PASSWORD")
|
|
SMTP_FROM=$(env_escape "$SMTP_FROM")
|
|
SMTP_TLS=${SMTP_TLS}
|
|
SMTP_SSL=${SMTP_SSL}
|
|
EOF
|
|
|
|
if [[ -n "${ACME_EMAIL:-}" ]]; then
|
|
echo "ACME_EMAIL=$(env_escape "$ACME_EMAIL")" >> "$env_file"
|
|
fi
|
|
|
|
chmod 600 "$env_file"
|
|
log ".env записан: $env_file"
|
|
}
|
|
|
|
ensure_files() {
|
|
title "Проверка файлов проекта"
|
|
local need=(docker-compose.yml Dockerfile requirements.txt bot.py tariffs.json caddy/Caddyfile)
|
|
local missing=0
|
|
for f in "${need[@]}"; do
|
|
if [[ ! -e "${SCRIPT_DIR}/${f}" ]]; then
|
|
err "Нет файла: $f"
|
|
missing=1
|
|
fi
|
|
done
|
|
if [[ "$missing" -eq 1 ]]; then
|
|
err "Запускай install.sh из корня репозитория VPN Service Bot"
|
|
exit 1
|
|
fi
|
|
log "Файлы проекта на месте"
|
|
}
|
|
|
|
update_caddyfile_email() {
|
|
if [[ -z "${ACME_EMAIL:-}" ]]; then
|
|
return
|
|
fi
|
|
if grep -qE '^\s*email\s+' "${SCRIPT_DIR}/caddy/Caddyfile" 2>/dev/null; then
|
|
log "Caddyfile уже содержит email — не трогаем"
|
|
return
|
|
fi
|
|
local caddy_bak="${SCRIPT_DIR}/caddy/Caddyfile.bak.$(date +%Y%m%d-%H%M%S)"
|
|
cp -a "${SCRIPT_DIR}/caddy/Caddyfile" "$caddy_bak"
|
|
cat > "${SCRIPT_DIR}/caddy/Caddyfile" <<EOF
|
|
{
|
|
email ${ACME_EMAIL}
|
|
}
|
|
|
|
{\$WEBAPP_DOMAIN:bot.example.com} {
|
|
encode gzip
|
|
reverse_proxy bot:8080
|
|
|
|
header {
|
|
X-Content-Type-Options nosniff
|
|
Referrer-Policy no-referrer-when-downgrade
|
|
}
|
|
|
|
log {
|
|
output stdout
|
|
}
|
|
}
|
|
EOF
|
|
log "Caddyfile: email для ACME = ${ACME_EMAIL} (бэкап: $(basename "$caddy_bak"))"
|
|
}
|
|
|
|
dns_hint() {
|
|
title "DNS"
|
|
local ip
|
|
ip="$(curl -4 -fsS --max-time 5 https://ifconfig.me 2>/dev/null || curl -4 -fsS --max-time 5 https://api.ipify.org 2>/dev/null || echo "?")"
|
|
info "Публичный IP сервера: ${ip}"
|
|
info "Создай A-запись: ${WEBAPP_DOMAIN} → ${ip}"
|
|
ask_yn "DNS уже указывает на этот сервер?" "y"
|
|
if [[ "$REPLY" != "y" ]]; then
|
|
warn "Без DNS Let's Encrypt не выдаст сертификат. Можно продолжить — Caddy повторит позже."
|
|
fi
|
|
}
|
|
|
|
start_stack() {
|
|
title "Сборка и запуск (Docker Compose)"
|
|
cd "$SCRIPT_DIR"
|
|
docker compose pull db caddy || true
|
|
docker compose up -d --build
|
|
log "Контейнеры запущены"
|
|
echo
|
|
docker compose ps
|
|
echo
|
|
info "Ждём health…"
|
|
sleep 5
|
|
local ok=0
|
|
for _ in $(seq 1 30); do
|
|
if docker compose exec -T bot curl -fsS http://127.0.0.1:8080/api/health >/dev/null 2>&1; then
|
|
ok=1
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
if [[ "$ok" -eq 1 ]]; then
|
|
log "API health OK (внутри контейнера)"
|
|
else
|
|
warn "Health пока не ответил — смотри: docker compose logs -f bot"
|
|
fi
|
|
}
|
|
|
|
print_done() {
|
|
title "Готово"
|
|
echo -e "${GREEN}Установка завершена.${NC}"
|
|
echo
|
|
echo " Кабинет: ${WEBAPP_URL}"
|
|
echo " Админка: ${WEBAPP_URL}/admin"
|
|
echo " Логин админа: ${WEB_ADMIN_LOGIN}"
|
|
echo " Пароль админа: (тот, что задал / сгенерирован)"
|
|
echo
|
|
echo "Дальше:"
|
|
echo " 1) @BotFather → /setdomain → ${WEBAPP_DOMAIN}"
|
|
echo " 2) @BotFather → Menu Button → Web App → ${WEBAPP_URL}"
|
|
echo " 3) Платежи: ${WEBAPP_URL}/admin → Платежи"
|
|
echo " 4) SMTP: ${WEBAPP_URL}/admin → SMTP"
|
|
echo " 5) Рост: ${WEBAPP_URL}/admin → Рост (промо, trial, бэкапы)"
|
|
echo
|
|
echo "Бэкапы Postgres (cron, опционально):"
|
|
echo " 0 3 * * * cd ${SCRIPT_DIR} && docker compose exec -T -e PGPASSWORD=\$POSTGRES_PASSWORD db pg_dump -U \$POSTGRES_USER -d \$POSTGRES_DB | gzip > backups/vpnbot-\$(date +\\%F).sql.gz"
|
|
echo
|
|
echo "Команды:"
|
|
echo " cd ${SCRIPT_DIR}"
|
|
echo " docker compose ps"
|
|
echo " docker compose logs -f bot"
|
|
echo " docker compose logs -f caddy"
|
|
echo " docker compose up -d --build"
|
|
echo
|
|
}
|
|
|
|
main() {
|
|
clear 2>/dev/null || true
|
|
echo -e "${BOLD}"
|
|
echo " VPN Service Bot — установщик"
|
|
echo " Docker · PostgreSQL · Caddy (Let's Encrypt SSL)"
|
|
echo -e "${NC}"
|
|
|
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
|
cat <<'HELP'
|
|
Использование:
|
|
sudo bash install.sh Интерактивная установка
|
|
sudo bash install.sh --reinstall Пересобрать и перезапустить без опроса
|
|
sudo bash install.sh --env-only Только пересоздать .env (опрос), без Docker up
|
|
|
|
Стек: db (Postgres 17) + bot + caddy (80/443, авто-SSL).
|
|
HELP
|
|
exit 0
|
|
fi
|
|
|
|
need_root
|
|
check_os
|
|
ensure_files
|
|
|
|
if [[ "${1:-}" == "--reinstall" ]]; then
|
|
if [[ ! -f "${SCRIPT_DIR}/.env" ]]; then
|
|
err "Нет .env — сначала полный install.sh"
|
|
exit 1
|
|
fi
|
|
# shellcheck disable=SC1091
|
|
set -a
|
|
# shellcheck source=/dev/null
|
|
source "${SCRIPT_DIR}/.env"
|
|
set +a
|
|
WEBAPP_DOMAIN="${WEBAPP_DOMAIN:-}"
|
|
WEBAPP_URL="${WEBAPP_URL:-https://${WEBAPP_DOMAIN}}"
|
|
WEB_ADMIN_LOGIN="${WEB_ADMIN_LOGIN:-admin}"
|
|
install_docker
|
|
start_stack
|
|
print_done
|
|
exit 0
|
|
fi
|
|
|
|
install_docker
|
|
check_ports
|
|
configure_firewall
|
|
collect_config
|
|
write_env
|
|
update_caddyfile_email
|
|
|
|
if [[ "${1:-}" == "--env-only" ]]; then
|
|
log "Только .env — контейнеры не трогали"
|
|
echo "Запуск: docker compose up -d --build"
|
|
exit 0
|
|
fi
|
|
|
|
dns_hint
|
|
start_stack
|
|
print_done
|
|
}
|
|
|
|
main "$@"
|