feat: add one-question VPS installer

This commit is contained in:
Maintainer
2026-10-14 17:25:10 +07:00
parent 1e1dfa7b92
commit 3f063dd2ca
6 changed files with 441 additions and 2 deletions
+294
View File
@@ -0,0 +1,294 @@
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
domain_value="${TPROXY_HOSTNAME:-}"
edge_network="telegram-web-proxy-edge"
usage() {
cat <<'USAGE'
Использование: sudo ./quick-install.sh [--domain ДОМЕН]
Быстрая установка Telegram WEB proxy и Caddy на чистый VPS.
Без параметров скрипт спросит только домен. DNS-запись A домена должна уже
указывать прямо на публичный IPv4 этого сервера.
Поддерживаются 64-битные Ubuntu и Debian на архитектуре x86_64.
USAGE
}
fail() {
printf 'Ошибка: %s\n' "$1" >&2
exit 1
}
while (($#)); do
case "$1" in
--domain)
(($# >= 2)) || fail "после --domain нужно указать домен"
domain_value="$2"
shift 2
;;
-h | --help)
usage
exit 0
;;
*)
printf 'Неизвестный параметр: %s\n\n' "$1" >&2
usage >&2
exit 2
;;
esac
done
if ((EUID != 0)); then
if command -v sudo >/dev/null 2>&1; then
if [[ -n "$domain_value" ]]; then
exec sudo bash "$0" --domain "$domain_value"
fi
exec sudo bash "$0"
fi
fail "запустите скрипт от root: sudo ./quick-install.sh"
fi
if [[ "$(uname -s)" != Linux || "$(uname -m)" != x86_64 ]]; then
fail "требуется 64-битный Linux x86_64"
fi
repository="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
for required_file in compose.yml compose.quick.yml Caddyfile Dockerfile; do
[[ -f "$repository/$required_file" ]] ||
fail "рядом со скриптом отсутствует $required_file; скачайте весь репозиторий"
done
if [[ ! -r /etc/os-release ]]; then
fail "не удалось определить операционную систему"
fi
# shellcheck disable=SC1091
source /etc/os-release
case "${ID:-}" in
ubuntu | debian) ;;
*) fail "поддерживаются только Ubuntu и Debian, получено: ${ID:-неизвестно}" ;;
esac
[[ -n "${VERSION_CODENAME:-}" ]] || fail "не удалось определить версию системы"
if [[ -z "$domain_value" ]]; then
[[ -t 0 ]] || fail "в автоматическом режиме укажите --domain proxy.example.com"
printf 'Введите домен прокси (например, proxy.example.com): '
read -r domain_value
fi
domain_value="$(printf '%s' "$domain_value" |
tr '[:upper:]' '[:lower:]' |
sed -E 's#^[[:space:]]*https?://##; s#/.*$##; s/:.*$//; s/[[:space:]]//g')"
valid_hostname() {
local value=$1 label
local -a labels
[[ ${#value} -le 253 && "$value" == *.* ]] || return 1
[[ "$value" =~ ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ ]] || return 1
local IFS=.
read -r -a labels <<<"$value"
for label in "${labels[@]}"; do
[[ -n "$label" && ${#label} -le 63 ]] || return 1
[[ "$label" != -* && "$label" != *- ]] || return 1
done
}
valid_ipv4() {
local ip=$1 octet
local -a octets
local IFS=.
read -r -a octets <<<"$ip"
[[ ${#octets[@]} -eq 4 ]] || return 1
for octet in "${octets[@]}"; do
[[ "$octet" =~ ^[0-9]{1,3}$ ]] || return 1
((10#$octet <= 255)) || return 1
done
}
public_ipv4() {
local ip=$1
local -a octets
local IFS=.
valid_ipv4 "$ip" || return 1
read -r -a octets <<<"$ip"
((10#${octets[0]} != 0)) || return 1
((10#${octets[0]} != 10)) || return 1
((10#${octets[0]} != 127)) || return 1
! ((10#${octets[0]} == 100 && 10#${octets[1]} >= 64 && 10#${octets[1]} <= 127)) || return 1
! ((10#${octets[0]} == 169 && 10#${octets[1]} == 254)) || return 1
! ((10#${octets[0]} == 172 && 10#${octets[1]} >= 16 && 10#${octets[1]} <= 31)) || return 1
! ((10#${octets[0]} == 192 && 10#${octets[1]} == 0)) || return 1
! ((10#${octets[0]} == 192 && 10#${octets[1]} == 88 && 10#${octets[2]} == 99)) || return 1
! ((10#${octets[0]} == 192 && 10#${octets[1]} == 168)) || return 1
! ((10#${octets[0]} == 198 && (10#${octets[1]} == 18 || 10#${octets[1]} == 19))) || return 1
! ((10#${octets[0]} == 198 && 10#${octets[1]} == 51 && 10#${octets[2]} == 100)) || return 1
! ((10#${octets[0]} == 203 && 10#${octets[1]} == 0 && 10#${octets[2]} == 113)) || return 1
((10#${octets[0]} < 224)) || return 1
}
valid_hostname "$domain_value" || fail "некорректный домен: $domain_value"
export DEBIAN_FRONTEND=noninteractive
printf '\n[1/6] Устанавливаю системные компоненты...\n'
apt-get update
apt-get install -y --no-install-recommends ca-certificates curl iproute2
mapfile -t resolved_ips < <(
getent ahostsv4 "$domain_value" 2>/dev/null |
awk '{print $1}' | sort -u
)
if ((${#resolved_ips[@]} == 0)); then
fail "домен $domain_value пока не имеет DNS-записи A"
fi
if ((${#resolved_ips[@]} != 1)); then
fail "для простой установки оставьте у домена одну DNS-запись A; найдено: ${resolved_ips[*]}"
fi
public_ip="${resolved_ips[0]}"
public_ipv4 "$public_ip" || fail "DNS домена указывает не на публичный IPv4: $public_ip"
printf 'DNS найден: %s -> %s\n' "$domain_value" "$public_ip"
install_docker() {
local -a conflicts=()
local package
for package in docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc; do
if dpkg-query -W -f='${db:Status-Abbrev}' "$package" 2>/dev/null | grep -q '^ii'; then
conflicts+=("$package")
fi
done
if ((${#conflicts[@]} > 0)); then
fail "обнаружены конфликтующие пакеты (${conflicts[*]}). Используйте чистый VPS или удалите их вручную"
fi
printf '\n[2/6] Устанавливаю Docker из официального репозитория...\n'
install -m 0755 -d /etc/apt/keyrings
curl --fail --silent --show-error --location \
--proto '=https' --proto-redir '=https' --tlsv1.2 \
"https://download.docker.com/linux/${ID}/gpg" \
-o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
cat >/etc/apt/sources.list.d/docker.sources <<SOURCES
Types: deb
URIs: https://download.docker.com/linux/${ID}
Suites: ${VERSION_CODENAME}
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
SOURCES
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
}
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
printf '\n[2/6] Docker и Docker Compose уже установлены.\n'
else
install_docker
fi
systemctl enable --now docker
docker info >/dev/null 2>&1 || fail "Docker установлен, но его служба не запустилась"
docker compose version >/dev/null 2>&1 || fail "плагин Docker Compose недоступен"
own_caddy_running="$(docker inspect --format '{{.State.Running}}' tg-web-proxy-caddy 2>/dev/null || true)"
busy_ports="$(ss -H -ltn 2>/dev/null | awk '$4 ~ /:(80|443)$/ {print $4}' | sort -u)"
if [[ -n "$busy_ports" && "$own_caddy_running" != true ]]; then
fail "порты 80 или 443 уже заняты: ${busy_ports//$'\n'/, }. Для быстрой установки нужен чистый VPS"
fi
printf '\n[3/6] Готовлю закрытую Docker-сеть и настройки...\n'
if ! docker network inspect "$edge_network" >/dev/null 2>&1; then
docker network create "$edge_network" >/dev/null
fi
environment_file="$repository/.env"
temporary_environment="$(mktemp "$repository/.env.new.XXXXXX")"
trap 'rm -f "$temporary_environment"' EXIT
cat >"$temporary_environment" <<ENV
TPROXY_HOSTNAME=$domain_value
MTPROXY_PUBLIC_IP=$public_ip
NPM_NETWORK_NAME=$edge_network
MTPROXY_WORKERS=1
MTPROXY_MAX_CONNECTIONS=4096
ENV
chmod 0600 "$temporary_environment"
mv -f "$temporary_environment" "$environment_file"
trap - EXIT
compose=(
docker compose
--env-file "$environment_file"
-f "$repository/compose.yml"
-f "$repository/compose.quick.yml"
)
printf '\n[4/6] Собираю Telegram WEB proxy...\n'
"${compose[@]}" config --quiet
"${compose[@]}" build --pull
"${compose[@]}" pull caddy
printf '\n[5/6] Запускаю прокси и автоматический HTTPS...\n'
"${compose[@]}" up -d --remove-orphans
proxy_health=
for _ in $(seq 1 90); do
proxy_health="$(docker inspect \
--format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \
tg-web-proxy 2>/dev/null || true)"
[[ "$proxy_health" == healthy ]] && break
if [[ "$proxy_health" == unhealthy || "$proxy_health" == exited || "$proxy_health" == dead ]]; then
break
fi
sleep 2
done
if [[ "$proxy_health" != healthy ]]; then
docker logs --tail 100 tg-web-proxy >&2 || true
fail "контейнер прокси не запустился (состояние: ${proxy_health:-неизвестно})"
fi
client_secret="$(docker exec tg-web-proxy sh -c 'tr -d "\\r\\n" < /data/client-secret')"
[[ "$client_secret" =~ ^[0-9a-f]{32}$ ]] || fail "не удалось прочитать клиентский секрет"
printf '\n[6/6] Жду выпуска HTTPS-сертификата'
https_ready=false
for _ in $(seq 1 60); do
if curl --fail --silent --show-error \
--connect-timeout 5 --max-time 15 \
--proto '=https' --tlsv1.2 \
"https://$domain_value/" >/dev/null 2>&1; then
https_ready=true
break
fi
printf '.'
sleep 5
done
printf '\n'
if [[ "$https_ready" != true ]]; then
docker logs --tail 100 tg-web-proxy-caddy >&2 || true
cat >&2 <<ERROR
Ошибка: HTTPS не стал доступен.
Проверьте, что DNS-запись A указывает на этот VPS и что у провайдера сервера
разрешены входящие TCP-порты 80 и 443. После исправления снова запустите:
sudo ./quick-install.sh --domain $domain_value
ERROR
exit 1
fi
cat <<RESULT
Готово: Telegram WEB proxy работает через HTTPS.
Параметры для Telegram:
Тип: WEB
Сервер: $domain_value
Секрет: $client_secret
Ссылка для подключения:
tg://webproxy?server=$domain_value&secret=$client_secret
Не публикуйте секрет и ссылку подключения. Для обновления проекта выполните
git pull --ff-only, затем снова запустите sudo ./quick-install.sh.
RESULT