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
+7
View File
@@ -0,0 +1,7 @@
{
admin off
}
{$TPROXY_HOSTNAME} {
reverse_proxy proxy:8088
}
+28
View File
@@ -26,7 +26,9 @@ WebSocket traffic always reach `tproxy-server`.
## Repository contents ## Repository contents
- `quick-install.sh` — fresh-VPS installation with Docker and automatic HTTPS;
- `install.sh` — interactive installation on an existing Docker/NPM host; - `install.sh` — interactive installation on an existing Docker/NPM host;
- `compose.quick.yml` and `Caddyfile` — optional Caddy frontend for the quick path;
- `compose.yml` and `Dockerfile` — reproducible, checksum-pinned build; - `compose.yml` and `Dockerfile` — reproducible, checksum-pinned build;
- `ingress/ingress.go` — canonical ingress used by new installations; - `ingress/ingress.go` — canonical ingress used by new installations;
- `ingress/ingress.js` — equivalent emergency implementation; - `ingress/ingress.js` — equivalent emergency implementation;
@@ -37,6 +39,32 @@ WebSocket traffic always reach `tproxy-server`.
## Installation ## Installation
### Quick installation on a fresh VPS
Use a clean x86_64 VPS running Ubuntu or Debian. Create one direct DNS `A`
record for the proxy hostname. Copy the HTTPS clone URL from the Gitea **Code**
menu, connect to the VPS over SSH, and run:
```sh
sudo apt update
sudo apt install -y git
git clone <REPOSITORY_URL>
cd telegram-web-proxy-stack
sudo ./quick-install.sh
```
The script asks only for the hostname. It resolves the public IPv4 from DNS,
installs Docker from Docker's official package repository, deploys the proxy and
Caddy, obtains the TLS certificate, and prints the Telegram connection values.
Inbound TCP ports 80 and 443 must be allowed by the VPS provider. This is a
separate deployment mode and does not require Nginx Proxy Manager.
Run `quick-install.sh` again after `git pull --ff-only` to update a quick
installation. Do not switch that deployment to `install.sh`, which is intended
for the Nginx Proxy Manager mode below.
### Existing Docker host with Nginx Proxy Manager
Requirements: Requirements:
- x86_64 Linux; - x86_64 Linux;
+69
View File
@@ -9,6 +9,75 @@
> Telegram, в котором есть тип прокси `WEB`. Если такого пункта в настройках > Telegram, в котором есть тип прокси `WEB`. Если такого пункта в настройках
> нет, установленная версия клиента этот транспорт ещё не поддерживает. > нет, установленная версия клиента этот транспорт ещё не поддерживает.
## Самый простой вариант: чистый VPS и один вопрос
Этот вариант подходит, если вы только что купили отдельный чистый VPS и не
хотите настраивать Nginx Proxy Manager. Скрипт сам установит Docker, запустит
прокси вместе с Caddy и получит HTTPS-сертификат. Он спросит только домен.
Понадобятся:
- 64-битный VPS `x86_64` с Ubuntu или Debian;
- один домен или поддомен с единственной DNS-записью `A`, которая уже указывает
прямо на публичный IPv4 этого VPS;
- открытые у провайдера сервера входящие TCP-порты `80` и `443`.
На странице проекта в Gitea нажмите **Code**, скопируйте адрес **HTTPS**, затем
подключитесь к VPS по SSH и выполните команды ниже. Вместо текста в угловых
скобках вставьте скопированный адрес:
```sh
sudo apt update
sudo apt install -y git
git clone <АДРЕС_РЕПОЗИТОРИЯ>
cd telegram-web-proxy-stack
sudo ./quick-install.sh
```
После запуска нужно ввести только подготовленный домен. Пароли или токены
Gitea для публичного репозитория не требуются.
Можно случайно вставить домен вместе с `https://` — установщик уберёт схему и
лишний путь. После проверки DNS он автоматически:
1. установит системные компоненты и Docker из официального репозитория;
2. соберёт Telegram WEB proxy;
3. запустит Caddy и выпустит HTTPS-сертификат;
4. покажет сервер, секрет и готовую ссылку подключения.
Для автоматического запуска домен разрешается передать сразу:
```sh
sudo ./quick-install.sh --domain proxy.example.com
```
Секрет сохраняется в Docker-томе и остаётся прежним при повторном запуске.
Для обновления используйте только быстрый установщик:
```sh
git pull --ff-only
sudo ./quick-install.sh
```
Если выпуск сертификата не завершился, проверьте DNS и правила входящего
трафика у провайдера VPS, затем запустите ту же команду ещё раз. Этот вариант
не использует Nginx Proxy Manager и не требует входа в какую-либо панель.
Остановить быструю установку, не удаляя секрет и сертификат:
```sh
docker compose --env-file .env -f compose.yml -f compose.quick.yml down
```
Повторно запустить:
```sh
docker compose --env-file .env -f compose.yml -f compose.quick.yml up -d
```
Ниже описан второй, отдельный вариант для сервера, где Nginx Proxy Manager уже
установлен.
## Схема работы ## Схема работы
```text ```text
+39
View File
@@ -0,0 +1,39 @@
services:
caddy:
image: caddy:2.11.7-alpine
container_name: tg-web-proxy-caddy
restart: unless-stopped
depends_on:
proxy:
condition: service_healthy
environment:
TPROXY_HOSTNAME: ${TPROXY_HOSTNAME:?set by quick-install.sh}
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- tg_webproxy_caddy_data:/data
- tg_webproxy_caddy_config:/config
networks:
- npm_network
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
pids_limit: 128
mem_limit: 256m
cpus: 0.50
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
tg_webproxy_caddy_data:
name: tg_webproxy_caddy_data
tg_webproxy_caddy_config:
name: tg_webproxy_caddy_config
+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
+4 -2
View File
@@ -52,7 +52,8 @@ scan_network_literals() {
[[ -z "$domain" ]] && continue [[ -z "$domain" ]] && continue
domain="$(printf '%s' "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:].-]$//')" domain="$(printf '%s' "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:].-]$//')"
case "$domain" in case "$domain" in
github.com | core.telegram.org | example.com | *.example.com | *.localhost | \ github.com | core.telegram.org | download.docker.com | docker.io | containerd.io | \
example.com | *.example.com | *.localhost | \
*.md | *.sh | *.json | *.yml | *.yaml | *.html | *.go | *.js | \ *.md | *.sh | *.json | *.yml | *.yaml | *.html | *.go | *.js | \
*.gz | *.pem | *.key | *.p12 | *.pfx | *.example) ;; *.gz | *.pem | *.key | *.p12 | *.pfx | *.example) ;;
*) report "unexpected hostname $domain in $context$file" ;; *) report "unexpected hostname $domain in $context$file" ;;
@@ -139,7 +140,8 @@ while IFS= read -r revision; do
[[ -z "$domain" ]] && continue [[ -z "$domain" ]] && continue
domain="$(printf '%s' "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:].-]$//')" domain="$(printf '%s' "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:].-]$//')"
case "$domain" in case "$domain" in
github.com | core.telegram.org | example.com | *.example.com | *.localhost | \ github.com | core.telegram.org | download.docker.com | docker.io | containerd.io | \
example.com | *.example.com | *.localhost | \
*.md | *.sh | *.json | *.yml | *.yaml | *.html | *.go | *.js | \ *.md | *.sh | *.json | *.yml | *.yaml | *.html | *.go | *.js | \
*.gz | *.pem | *.key | *.p12 | *.pfx | *.example) ;; *.gz | *.pem | *.key | *.p12 | *.pfx | *.example) ;;
*) report "unexpected hostname $domain in Git history ${revision:0:12}" ;; *) report "unexpected hostname $domain in Git history ${revision:0:12}" ;;