Files
telegram-shop/install.sh
NW 5c7bc242c5 ci: registry-based releases — multi-arch images built in CI, servers only pull
- release workflow: buildx multi-arch (amd64+arm64) build+push of bot and admin images to Gitea Container Registry (git.softuniq.eu/telegram-market/telegram-shop[-admin])
- docker-compose: bot/admin use registry images (IMAGE_TAG for pinning); tor-proxy stays local alpine build
- install.sh: pull from registry instead of local docker build (no native compilation on weak hardware); interactive .env setup preserved
- new buildx runner gitea-host-buildx (label buildx) on x86_64 host
- REGISTRY_TOKEN replaced with package-scoped token (write:package)
- README/VERSION/.env.example updated
2026-08-11 10:37:09 +01:00

442 lines
19 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
set -euo pipefail
# ============================================================
# Telegram Shop — установщик
# Скрипт для развёртывания на x86_64 и ARM64 (Orange Pi, RPi)
# Совместим с POSIX sh (Alpine ash)
# ============================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
print_header() {
printf "${CYAN}${BOLD}\n"
printf "╔═══════════════════════════════════════════════════════════════╗\n"
printf "║ Telegram Shop — Установщик v1.1 ║\n"
printf "║ Поддержка x86_64 и ARM64 (Orange Pi, Raspberry Pi) ║\n"
printf "╚═══════════════════════════════════════════════════════════════╝\n"
printf "${NC}\n"
}
print_ok() { printf "${GREEN}${NC} %s\n" "$1"; }
print_warn() { printf "${YELLOW}${NC} %s\n" "$1"; }
print_err() { printf "${RED}${NC} %s\n" "$1"; }
print_info() { printf "${BLUE}${NC} %s\n" "$1"; }
print_step() { printf "\n${CYAN}${BOLD}▶ %s${NC}\n" "$1"; }
trap 'print_err "Установка прервана ошибкой (строка $LINENO)"; exit 1' ERR
# ============================================================
# 0. Определение окружения
# ============================================================
print_header
print_step "Определение окружения"
ARCH=$(uname -m)
case "$ARCH" in
x86_64) ARCH_NAME="x86_64 (Intel/AMD)" ;;
aarch64|arm64) ARCH_NAME="ARM64 (Orange Pi / RPi)" ;;
armv7l) ARCH_NAME="ARMv7 (Raspberry Pi 2)" ;;
*)
print_err "Неподдерживаемая архитектура: $ARCH"
exit 1
;;
esac
print_ok "Архитектура: $ARCH_NAME"
print_info "Docker автоматически соберёт образ под текущую архитектуру"
# ============================================================
# 1. Проверка / установка Docker
# ============================================================
print_step "Проверка Docker"
DOCKER_MISSING=0
if ! command -v docker >/dev/null 2>&1; then
DOCKER_MISSING=1
elif ! docker version >/dev/null 2>&1; then
DOCKER_MISSING=1
fi
if [ "$DOCKER_MISSING" -eq 1 ]; then
print_warn "Docker не установлен или не запущен"
printf "\n"
printf "Установить Docker сейчас? (y/N): "
read -r INSTALL_DOCKER
case "$INSTALL_DOCKER" in
[Yy]|[Yy][Ee][Ss])
;;
*)
print_err "Docker обязателен. Установите вручную: https://docs.docker.com/engine/install/"
exit 1
;;
esac
print_info "Установка Docker..."
if command -v apk >/dev/null 2>&1; then
# Alpine
apk add --no-cache docker docker-cli-compose
rc-service docker start 2>/dev/null || service docker start 2>/dev/null || true
if ! docker version >/dev/null 2>&1; then
print_err "Docker не запустился. Запустите вручную: rc-service docker start"
exit 1
fi
elif command -v apt-get >/dev/null 2>&1; then
# Debian / Ubuntu / Armbian
apt-get update
apt-get install -y ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL "https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/gpg" | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
printf "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$(. /etc/os-release && echo "$ID") $(. /etc/os-release && echo "$VERSION_CODENAME") stable\n" \
> /etc/apt/sources.list.d/docker.list
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
service docker start 2>/dev/null || systemctl start docker 2>/dev/null || true
if ! docker version >/dev/null 2>&1; then
print_err "Docker не запустился. Запустите вручную: systemctl start docker"
exit 1
fi
else
print_err "Неизвестный пакетный менеджер. Установите Docker вручную."
exit 1
fi
print_ok "Docker установлен"
else
print_ok "Docker установлен: $(docker --version)"
fi
# ============================================================
# 2. Проверка docker compose
# ============================================================
print_step "Проверка docker compose"
COMPOSE_CMD=""
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD="docker compose"
COMPOSE_VER=$(docker compose version --short 2>/dev/null || echo "v2")
print_ok "docker compose v2 доступен: $COMPOSE_VER"
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD="docker-compose"
print_ok "docker-compose v1 доступен: $(docker-compose --version)"
else
print_err "docker compose не найден. Установите docker-compose-plugin или docker-compose."
exit 1
fi
# ============================================================
# 3. Подготовка файлов окружения
# ============================================================
print_step "Подготовка .env"
if [ ! -f ".env" ]; then
if [ -f ".env.example" ]; then
cp .env.example .env
# Убираем CRLF если файл скопирован из Windows
if command -v sed >/dev/null 2>&1; then
sed -i 's/\r$//' .env
fi
print_ok ".env создан из .env.example"
else
print_err ".env.example не найден!"
exit 1
fi
else
print_ok ".env уже существует — пропускаю создание"
# Убираем CRLF в существующем файле тоже
if command -v sed >/dev/null 2>&1; then
sed -i 's/\r$//' .env
fi
fi
# ============================================================
# 4. Настройка переменных окружения (интерактивно)
# ============================================================
print_step "Настройка переменных окружения (интерактивно)"
# Функция для обновления переменной в .env (безопасно для спецсимволов в значении)
update_env_var() {
var_name="$1"
var_value="$2"
if grep -q "^${var_name}=" .env 2>/dev/null; then
awk -v key="$var_name" -v val="$var_value" '
BEGIN { FS = OFS = "="; done = 0 }
/^[A-Za-z_][A-Za-z0-9_]*=/ {
if ($1 == key) { print key "=" val; done = 1; next }
}
{ print }
END { if (!done) print key "=" val }
' .env > .env.tmp && mv .env.tmp .env
else
echo "${var_name}=${var_value}" >> .env
fi
}
# Скрытый ввод секретов (POSIX sh, работает в Alpine ash)
read_secret() {
printf "%s" "$1"
stty -echo 2>/dev/null
read -r input
stty echo 2>/dev/null
printf "\n"
}
# Функция для генерации случайного hex-строки (32 байта)
generate_random_hex() {
if command -v node >/dev/null 2>&1; then
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
elif command -v openssl >/dev/null 2>&1; then
openssl rand -hex 32
else
od -An -tx32 /dev/urandom | tr -d ' \n'
fi
}
# BOT_TOKEN
current_bot_token=$(grep -E "^BOT_TOKEN=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
if [ -z "$current_bot_token" ] || [ "$current_bot_token" = "your_bot_token_here" ]; then
print_warn "BOT_TOKEN не задан или содержит placeholder."
read_secret "Введите BOT_TOKEN (от @BotFather): "
update_env_var "BOT_TOKEN" "$input"
bot_token="$input"
else
bot_token="$current_bot_token"
fi
# ADMIN_IDS
current_admin_ids=$(grep -E "^ADMIN_IDS=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
if [ -z "$current_admin_ids" ] || [ "$current_admin_ids" = "123456789,987654321" ]; then
print_warn "ADMIN_IDS не задан или содержит placeholder."
printf "Введите ADMIN_IDS (через запятую, например: 123456789,987654321): "
read -r input
update_env_var "ADMIN_IDS" "$input"
admin_ids="$input"
else
admin_ids="$current_admin_ids"
fi
# ENCRYPTION_KEY
current_encryption_key=$(grep -E "^ENCRYPTION_KEY=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
if [ -z "$current_encryption_key" ]; then
encryption_key=$(generate_random_hex)
update_env_var "ENCRYPTION_KEY" "$encryption_key"
print_ok "ENCRYPTION_KEY сгенерирован автоматически"
else
encryption_key="$current_encryption_key"
fi
# ADMIN_SECRET
current_admin_secret=$(grep -E "^ADMIN_SECRET=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
if [ -z "$current_admin_secret" ] || [ "$current_admin_secret" = "changeme" ]; then
admin_secret=$(generate_random_hex)
update_env_var "ADMIN_SECRET" "$admin_secret"
print_ok "ADMIN_SECRET сгенерирован автоматически"
else
admin_secret="$current_admin_secret"
fi
# SUPER_ADMIN_SECRET
current_super_admin_secret=$(grep -E "^SUPER_ADMIN_SECRET=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
if [ -z "$current_super_admin_secret" ] || [ "$current_super_admin_secret" = "changeme_super" ]; then
# Генерация до тех пор, пока не получим значение, отличное от ADMIN_SECRET
super_admin_secret=""
while [ -z "$super_admin_secret" ] || [ "$super_admin_secret" = "$admin_secret" ]; do
super_admin_secret=$(generate_random_hex)
done
update_env_var "SUPER_ADMIN_SECRET" "$super_admin_secret"
print_ok "SUPER_ADMIN_SECRET сгенерирован автоматически"
else
super_admin_secret="$current_super_admin_secret"
fi
# SHOP_ACTIVATED
current_shop_activated=$(grep -E "^SHOP_ACTIVATED=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
if [ -z "$current_shop_activated" ]; then
shop_activated="false"
update_env_var "SHOP_ACTIVATED" "$shop_activated"
print_info "SHOP_ACTIVATED установлен в 'false' (магазин стартует заблокированным)"
else
shop_activated="$current_shop_activated"
fi
# ADMIN_URL
current_admin_url=$(grep -E "^ADMIN_URL=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
if [ -z "$current_admin_url" ]; then
# Попытка определить LAN IP
lan_ip=$(hostname -I | awk '{print $1}')
if [ -z "$lan_ip" ]; then
lan_ip=$(ip route get 1 | awk '{print $7}' 2>/dev/null)
fi
if [ -n "$lan_ip" ]; then
admin_url="http://${lan_ip}:3000"
update_env_var "ADMIN_URL" "$admin_url"
print_info "ADMIN_URL установлен в $admin_url (определен автоматически)"
else
print_warn "Не удалось определить LAN IP. ADMIN_URL не установлен."
fi
else
admin_url="$current_admin_url"
fi
# Ограничиваем доступ к .env (содержит секреты)
if [ -f .env ]; then
chmod 600 .env
print_ok ".env защищён (chmod 600)"
fi
# ============================================================
# 5. Создание директорий
# ============================================================
print_step "Создание директорий"
mkdir -p db uploads
print_ok "db/, uploads/ готовы"
# ============================================================
# 6. Загрузка образов из Gitea Container Registry
# ============================================================
print_step "Загрузка образов из Gitea Container Registry"
print_info "Архитектура: $ARCH_NAME"
print_info "Образы собираются в CI (Gitea Actions, buildx multi-arch) — на сервере ничего не компилируется"
# Релизная версия: по умолчанию latest, можно переопределить IMAGE_TAG=v1.2.9
IMAGE_TAG="${IMAGE_TAG:-latest}"
print_info "Используемая версия образа: $IMAGE_TAG"
$COMPOSE_CMD pull telegram_shop_prod tg_shop_admin
print_ok "Образы telegram-shop:${IMAGE_TAG} и telegram-shop-admin:${IMAGE_TAG} загружены"
# ============================================================
# 7. Запуск контейнеров
# ============================================================
print_step "Запуск контейнеров"
IMAGE_TAG="${IMAGE_TAG}" $COMPOSE_CMD up -d
print_ok "Контейнеры запущены"
# ============================================================
# 8. Проверка статуса
# ============================================================
print_step "Проверка статуса"
sleep 3
$COMPOSE_CMD ps
# ============================================================
# 9. Показ логов
# ============================================================
print_step "Последние логи"
$COMPOSE_CMD logs --tail=20
# ============================================================
# 10. Health-check с повторными попытками
# ============================================================
print_step "Проверка работоспособности"
HEALTH_URL="http://localhost:3001/health"
print_info "Запрос: $HEALTH_URL"
HEALTH_OK=0
for ATTEMPT in 1 2 3 4 5 6; do
sleep 5
if curl -sf "$HEALTH_URL" >/dev/null 2>&1; then
HEALTH_OK=1
break
fi
print_info "Попытка $ATTEMPT/6 — сервис ещё запускается..."
done
if [ "$HEALTH_OK" -eq 1 ]; then
RESPONSE=$(curl -sf "$HEALTH_URL" 2>/dev/null || echo "ok")
print_ok "Сервис отвечает!"
printf " ${GREEN}Ответ: %s${NC}\n" "$RESPONSE"
else
print_warn "Health-check не ответил за 30 секунд. Проверьте логи:"
printf " curl %s\n" "$HEALTH_URL"
printf " %s logs -f\n" "$COMPOSE_CMD"
fi
# ============================================================
# 11. Tor Proxy — onion-адреса
# ============================================================
printf "\n"
print_step "Проверка Tor-прокси"
TOR_RUNNING=0
for ATTEMPT in 1 2 3 4 5 6; do
sleep 5
if docker exec tor-proxy test -s /var/lib/tor/ssh/hostname 2>/dev/null && \
docker exec tor-proxy test -s /var/lib/tor/admin/hostname 2>/dev/null; then
TOR_RUNNING=1
break
fi
print_info "Попытка $ATTEMPT/6 — Tor генерирует onion-адреса..."
done
if [ "$TOR_RUNNING" -eq 1 ]; then
SSH_ONION=$(docker exec tor-proxy cat /var/lib/tor/ssh/hostname 2>/dev/null || echo "")
ADMIN_ONION=$(docker exec tor-proxy cat /var/lib/tor/admin/hostname 2>/dev/null || echo "")
if [ -n "$SSH_ONION" ] && [ -n "$ADMIN_ONION" ]; then
print_ok "Tor-прокси работает!"
printf "\n ${CYAN}${BOLD}SSH onion:${NC} %s\n" "$SSH_ONION"
printf " ${CYAN}${BOLD}Admin onion:${NC} %s\n" "$ADMIN_ONION"
printf "\n ${BOLD}Подключение:${NC}\n"
printf " SSH: torify ssh root@%s\n" "$SSH_ONION"
printf " Admin: откройте http://%s в Tor Browser\n" "$ADMIN_ONION"
if [ -f "./tor-proxy/get-onions.sh" ]; then
sh ./tor-proxy/get-onions.sh 2>/dev/null || true
fi
else
print_warn "Tor запущен, но onion-адреса не найдены"
fi
else
print_warn "Tor-прокси не стартовал за 30 секунд. Проверьте логи:"
printf " docker logs tor-proxy\n"
fi
# ============================================================
# 12. Финальная сводка
# ============================================================
printf "\n"
print_step "Финальная сводка"
# LAN IP для админки (если не определен ранее)
if [ -z "${admin_url:-}" ]; then
lan_ip=$(hostname -I | awk '{print $1}')
if [ -z "$lan_ip" ]; then
lan_ip=$(ip route get 1 | awk '{print $7}' 2>/dev/null)
fi
if [ -n "$lan_ip" ]; then
admin_url="http://${lan_ip}:3000"
else
admin_url="Не определен"
fi
fi
printf " ${CYAN}${BOLD}LAN URL админки:${NC} %s\n" "$admin_url"
printf " ${CYAN}${BOLD}SSH onion:${NC} %s\n" "$SSH_ONION"
printf " ${CYAN}${BOLD}Admin onion:${NC} %s\n" "$ADMIN_ONION"
printf " ${CYAN}${BOLD}ADMIN_SECRET:${NC} %s\n" "$admin_secret"
printf " ${CYAN}${BOLD}SUPER_ADMIN_SECRET:${NC} %s\n" "$super_admin_secret"
printf "\n"
printf "${YELLOW}⚠ Магазин стартует заблокированным (SHOP_ACTIVATED=false).${NC}\n"
printf "${YELLOW} Войдите как супер-админ, добавьте комиссионные кошельки и активируйте магазин в Настройках.${NC}\n"
printf "\n"
# ============================================================
# Готово
# ============================================================
printf "\n"
printf "${GREEN}${BOLD}╔═══════════════════════════════════════════════════════════════╗${NC}\n"
printf "${GREEN}${BOLD}║ Установка завершена! ║${NC}\n"
printf "${GREEN}${BOLD}╚═══════════════════════════════════════════════════════════════╝${NC}\n"
printf "\n"
printf "${BOLD}Полезные команды:${NC}\n"
printf " docker compose ps # статус контейнеров\n"
printf " docker compose logs -f # логи в реальном времени\n"
printf " docker compose restart # перезапустить\n"
printf " docker compose down # остановить\n"
printf " docker compose up -d --build # пересобрать и запустить\n"
printf "\n"
printf "${BOLD}Health-check:${NC} %s\n" "$HEALTH_URL"