- 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
442 lines
19 KiB
Bash
Executable File
442 lines
19 KiB
Bash
Executable File
#!/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" |