feat(deploy): адрес привязки для прокси по туннелю и загрузка сертификата TURN с внешнего узла

- compose: app и LiveKit публикуются на GLCHAT_APP_BIND/GLCHAT_LIVEKIT_BIND
  (по умолчанию 127.0.0.1, для схемы «VPS-прокси + домашний сервер» — адрес в WireGuard)
- install.sh: --app-bind-addr/--livekit-bind-addr, --turn-cert-remote* для
  загрузки сертификата по SSH, glchat-cert-refresh умеет тянуть его с внешнего узла
This commit is contained in:
2026-09-19 20:28:29 +03:00
parent e8e063e52d
commit 1359821799
3 changed files with 91 additions and 12 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ services:
environment: environment:
<<: *app-env <<: *app-env
ports: ports:
- "127.0.0.1:${APP_PORT}:8080" - "${GLCHAT_APP_BIND}:${APP_PORT}:8080"
volumes: volumes:
- "${GLCHAT_DATA_DIR}/db:/app/data/db" - "${GLCHAT_DATA_DIR}/db:/app/data/db"
- "${GLCHAT_DATA_DIR}/files:/app/data/files" - "${GLCHAT_DATA_DIR}/files:/app/data/files"
+5 -4
View File
@@ -35,7 +35,7 @@ services:
environment: environment:
<<: *app-env <<: *app-env
ports: ports:
- "127.0.0.1:${APP_PORT}:8080" - "${GLCHAT_APP_BIND}:${APP_PORT}:8080"
volumes: volumes:
- "${GLCHAT_DATA_DIR}/db:/app/data/db" - "${GLCHAT_DATA_DIR}/db:/app/data/db"
- "${GLCHAT_DATA_DIR}/files:/app/data/files" - "${GLCHAT_DATA_DIR}/files:/app/data/files"
@@ -65,9 +65,10 @@ services:
environment: environment:
LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}" LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}"
ports: ports:
- "127.0.0.1:${LIVEKIT_WS_PORT}:7880" - "${GLCHAT_LIVEKIT_BIND}:${LIVEKIT_WS_PORT}:7880"
- "${LIVEKIT_TCP_PORT}:7881" # Медиапорты: тот же адрес привязки (для инстанса за NAT это адрес туннеля).
- "${LIVEKIT_UDP_PORT}:7882/udp" - "${GLCHAT_LIVEKIT_BIND}:${LIVEKIT_TCP_PORT}:7881"
- "${GLCHAT_LIVEKIT_BIND}:${LIVEKIT_UDP_PORT}:7882/udp"
volumes: volumes:
- ./livekit/livekit.yaml:/etc/livekit.yaml:ro - ./livekit/livekit.yaml:/etc/livekit.yaml:ro
- ${GLCHAT_TURN_CERT_DIR}:/etc/certs:ro - ${GLCHAT_TURN_CERT_DIR}:/etc/certs:ro
+85 -7
View File
@@ -61,6 +61,13 @@ TURN_RELAY_HOST="${TURN_RELAY_HOST:-}"
TURN_RELAY_PORT="${TURN_RELAY_PORT:-3478}" TURN_RELAY_PORT="${TURN_RELAY_PORT:-3478}"
TURN_RELAY_TLS_PORT="${TURN_RELAY_TLS_PORT:-5349}" TURN_RELAY_TLS_PORT="${TURN_RELAY_TLS_PORT:-5349}"
TLS_BY_PROXY="${TLS_BY_PROXY:-false}" TLS_BY_PROXY="${TLS_BY_PROXY:-false}"
APP_BIND_ADDR="${APP_BIND_ADDR:-127.0.0.1}"
LIVEKIT_BIND_ADDR="${LIVEKIT_BIND_ADDR:-127.0.0.1}"
TURN_CERT_REMOTE="${TURN_CERT_REMOTE:-}"
TURN_CERT_REMOTE_CRT="${TURN_CERT_REMOTE_CRT:-}"
TURN_CERT_REMOTE_KEY="${TURN_CERT_REMOTE_KEY:-}"
TURN_CERT_SSH_KEY="${TURN_CERT_SSH_KEY:-}"
TURN_CERT_SSH_PORT="${TURN_CERT_SSH_PORT:-22}"
TURN_TLS_PORT="${TURN_TLS_PORT:-5349}" TURN_TLS_PORT="${TURN_TLS_PORT:-5349}"
PROXY_MODE="${PROXY_MODE:-embedded}" PROXY_MODE="${PROXY_MODE:-embedded}"
PROFILE="${PROFILE:-auto}" PROFILE="${PROFILE:-auto}"
@@ -126,6 +133,14 @@ install.sh — установка glchat (AGENT.md 10).
--turn-relay-tls-port <port> TLS-порт внешнего релея (по умолчанию 5349) --turn-relay-tls-port <port> TLS-порт внешнего релея (по умолчанию 5349)
--tls-terminated-by-proxy TLS терминирует внешний прокси: свои 80/443 не занимаем, --tls-terminated-by-proxy TLS терминирует внешний прокси: свои 80/443 не занимаем,
но ссылки в API отдаются как https/wss но ссылки в API отдаются как https/wss
--app-bind-addr <addr> адрес app (по умолчанию 127.0.0.1); для прокси по
WireGuard укажите адрес в туннеле, например 10.77.0.13
--livekit-bind-addr <addr> адрес LiveKit (по умолчанию 127.0.0.1)
--turn-cert-remote <user@host> тянуть сертификат TURN/TLS с внешнего узла по SSH
--turn-cert-remote-crt <путь> путь к сертификату на внешнем узле
--turn-cert-remote-key <путь> путь к ключу на внешнем узле
--turn-cert-ssh-key <файл> приватный SSH-ключ для загрузки сертификата
--turn-cert-ssh-port <порт> SSH-порт внешнего узла (по умолчанию 22)
--turn-tls <port> TURN TLS (по умолчанию 5349) --turn-tls <port> TURN TLS (по умолчанию 5349)
--external-proxy не занимать 80/443: отдать маршруты внешнему прокси (AGENT.md 10.13) --external-proxy не занимать 80/443: отдать маршруты внешнему прокси (AGENT.md 10.13)
--no-files-subdomain не использовать files.<domain> (fallback /files/*) --no-files-subdomain не использовать files.<domain> (fallback /files/*)
@@ -177,6 +192,13 @@ parse_args() {
--turn-relay-port) TURN_RELAY_PORT="${2:?}"; shift 2 ;; --turn-relay-port) TURN_RELAY_PORT="${2:?}"; shift 2 ;;
--turn-relay-tls-port) TURN_RELAY_TLS_PORT="${2:?}"; shift 2 ;; --turn-relay-tls-port) TURN_RELAY_TLS_PORT="${2:?}"; shift 2 ;;
--tls-terminated-by-proxy) TLS_BY_PROXY="true"; shift ;; --tls-terminated-by-proxy) TLS_BY_PROXY="true"; shift ;;
--app-bind-addr) APP_BIND_ADDR="${2:?}"; shift 2 ;;
--livekit-bind-addr) LIVEKIT_BIND_ADDR="${2:?}"; shift 2 ;;
--turn-cert-remote) TURN_CERT_REMOTE="${2:?}"; shift 2 ;;
--turn-cert-remote-crt) TURN_CERT_REMOTE_CRT="${2:?}"; shift 2 ;;
--turn-cert-remote-key) TURN_CERT_REMOTE_KEY="${2:?}"; shift 2 ;;
--turn-cert-ssh-key) TURN_CERT_SSH_KEY="${2:?}"; shift 2 ;;
--turn-cert-ssh-port) TURN_CERT_SSH_PORT="${2:?}"; shift 2 ;;
--turn-tls) TURN_TLS_PORT="${2:?}"; shift 2 ;; --turn-tls) TURN_TLS_PORT="${2:?}"; shift 2 ;;
--profile) PROFILE="${2:?}"; shift 2 ;; --profile) PROFILE="${2:?}"; shift 2 ;;
--max-upload) MAX_UPLOAD_MB="${2:?}"; shift 2 ;; --max-upload) MAX_UPLOAD_MB="${2:?}"; shift 2 ;;
@@ -293,10 +315,13 @@ resolve_derived_values() {
GLCHAT_DATA_DIR="${GLCHAT_DATA_DIR}" GLCHAT_DATA_DIR="${GLCHAT_DATA_DIR}"
GLCHAT_CADDY_PROFILE="${CADDY_PROFILE}" GLCHAT_CADDY_PROFILE="${CADDY_PROFILE}"
GLCHAT_TURN_CERT_DIR="${TURN_CERT_DIR}" GLCHAT_TURN_CERT_DIR="${TURN_CERT_DIR}"
GLCHAT_APP_BIND="${APP_BIND_ADDR}"
GLCHAT_LIVEKIT_BIND="${LIVEKIT_BIND_ADDR}"
export FILES_DOMAIN_RESOLVED CADDY_PROFILE LIVEKIT_PUBLISH TURN_EXTERNAL_TLS \ export FILES_DOMAIN_RESOLVED CADDY_PROFILE LIVEKIT_PUBLISH TURN_EXTERNAL_TLS \
livekit_extra_codecs \ livekit_extra_codecs \
PROFILE_LIVEKIT_DISABLE_SIMULCAST PROFILE_LIVEKIT_USE_EXTERNAL_IP \ PROFILE_LIVEKIT_DISABLE_SIMULCAST PROFILE_LIVEKIT_USE_EXTERNAL_IP \
GLCHAT_DATA_DIR GLCHAT_CADDY_PROFILE GLCHAT_TURN_CERT_DIR \ GLCHAT_DATA_DIR GLCHAT_CADDY_PROFILE GLCHAT_TURN_CERT_DIR \
GLCHAT_APP_BIND GLCHAT_LIVEKIT_BIND \
TURN_DOMAIN_RESOLVED TURN_CERT_DIR TURN_RELAY_HOST_RESOLVED TURN_SECRET_RESOLVED TURN_DOMAIN_RESOLVED TURN_CERT_DIR TURN_RELAY_HOST_RESOLVED TURN_SECRET_RESOLVED
} }
@@ -317,8 +342,10 @@ validate_inputs() {
fi fi
# LiveKit требует сертификат для TURN/TLS (проверено на v1.9.7: без него # LiveKit требует сертификат для TURN/TLS (проверено на v1.9.7: без него
# сервер не стартует). Либо передаём файлы, либо отключаем TURN. # сервер не стартует). Либо передаём файлы, либо отключаем TURN.
if [ "$TURN_ENABLED" = "true" ] && [ "$DRY_RUN" != "1" ] && [ -z "$TURN_CERT_SRC" ]; then if [ "$TURN_ENABLED" = "true" ] && [ "$DRY_RUN" != "1" ] &&
die "TURN включён, но не задан сертификат: передайте --turn-cert/--turn-key или запустите с --no-turn" [ -z "$TURN_CERT_SRC" ] && [ -z "$TURN_CERT_REMOTE" ] &&
[ ! -f "${TARGET_DIR}/certs/turn.crt" ]; then
die "TURN включён, но сертификат не задан: используйте --turn-cert/--turn-key, --turn-cert-remote или --no-turn"
fi fi
} }
@@ -548,6 +575,8 @@ TLS_BY_PROXY=${TLS_BY_PROXY}
GLCHAT_DATA_DIR=${GLCHAT_DATA_DIR} GLCHAT_DATA_DIR=${GLCHAT_DATA_DIR}
GLCHAT_CADDY_PROFILE=${CADDY_PROFILE} GLCHAT_CADDY_PROFILE=${CADDY_PROFILE}
GLCHAT_TURN_CERT_DIR=${GLCHAT_TURN_CERT_DIR} GLCHAT_TURN_CERT_DIR=${GLCHAT_TURN_CERT_DIR}
GLCHAT_APP_BIND=${GLCHAT_APP_BIND}
GLCHAT_LIVEKIT_BIND=${GLCHAT_LIVEKIT_BIND}
TURN_RELAY_HOST=${TURN_RELAY_HOST_RESOLVED} TURN_RELAY_HOST=${TURN_RELAY_HOST_RESOLVED}
TURN_RELAY_PORT=${TURN_RELAY_PORT} TURN_RELAY_PORT=${TURN_RELAY_PORT}
TURN_RELAY_TLS_PORT=${TURN_RELAY_TLS_PORT} TURN_RELAY_TLS_PORT=${TURN_RELAY_TLS_PORT}
@@ -722,6 +751,28 @@ turn_shared_secret() {
# install_turn_certificate копирует сертификат TURN/TLS туда, где его читает # install_turn_certificate копирует сертификат TURN/TLS туда, где его читает
# контейнер LiveKit. Продление выполняет glchat-cert-refresh.timer. # контейнер LiveKit. Продление выполняет glchat-cert-refresh.timer.
fetch_remote_turn_certificate() {
[ -n "$TURN_CERT_REMOTE" ] || return 1
local ssh_opts=(-o BatchMode=yes -o StrictHostKeyChecking=accept-new -p "$TURN_CERT_SSH_PORT")
[ -n "$TURN_CERT_SSH_KEY" ] && ssh_opts+=(-i "$TURN_CERT_SSH_KEY")
command -v ssh >/dev/null 2>&1 || {
warn "ssh не найден: не могу забрать сертификат с ${TURN_CERT_REMOTE}"
return 1
}
ssh "${ssh_opts[@]}" "$TURN_CERT_REMOTE" "cat '${TURN_CERT_REMOTE_CRT}'" >"${TURN_CERT_DIR}/turn.crt" 2>/dev/null || {
warn "не удалось получить сертификат с ${TURN_CERT_REMOTE}:${TURN_CERT_REMOTE_CRT}"
rm -f "${TURN_CERT_DIR}/turn.crt"
return 1
}
ssh "${ssh_opts[@]}" "$TURN_CERT_REMOTE" "cat '${TURN_CERT_REMOTE_KEY}'" >"${TURN_CERT_DIR}/turn.key" 2>/dev/null || {
warn "не удалось получить ключ с ${TURN_CERT_REMOTE}:${TURN_CERT_REMOTE_KEY}"
rm -f "${TURN_CERT_DIR}/turn.key"
return 1
}
chmod 640 "${TURN_CERT_DIR}/turn.crt" "${TURN_CERT_DIR}/turn.key"
return 0
}
install_turn_certificate() { install_turn_certificate() {
[ "$TURN_ENABLED" = "true" ] || return 0 [ "$TURN_ENABLED" = "true" ] || return 0
if [ "$DRY_RUN" = "1" ]; then if [ "$DRY_RUN" = "1" ]; then
@@ -730,6 +781,11 @@ install_turn_certificate() {
fi fi
mkdir -p "$TURN_CERT_DIR" mkdir -p "$TURN_CERT_DIR"
chmod 750 "$TURN_CERT_DIR" chmod 750 "$TURN_CERT_DIR"
if [ -n "$TURN_CERT_REMOTE" ]; then
fetch_remote_turn_certificate && ok "сертификат TURN/TLS загружен с ${TURN_CERT_REMOTE}" ||
warn "сертификат TURN/TLS не загружен: TURN не запустится"
return 0
fi
if [ -n "$TURN_CERT_SRC" ]; then if [ -n "$TURN_CERT_SRC" ]; then
install -m 640 "$TURN_CERT_SRC" "${TURN_CERT_DIR}/turn.crt" install -m 640 "$TURN_CERT_SRC" "${TURN_CERT_DIR}/turn.crt"
install -m 640 "${TURN_CERT_SRC_KEY:?нужен --turn-key}" "${TURN_CERT_DIR}/turn.key" install -m 640 "${TURN_CERT_SRC_KEY:?нужен --turn-key}" "${TURN_CERT_DIR}/turn.key"
@@ -776,6 +832,7 @@ render_all_templates() {
"CADDY_IMAGE_TAG=${CADDY_IMAGE_TAG}" "LIVEKIT_IMAGE_TAG=${LIVEKIT_IMAGE_TAG}" \ "CADDY_IMAGE_TAG=${CADDY_IMAGE_TAG}" "LIVEKIT_IMAGE_TAG=${LIVEKIT_IMAGE_TAG}" \
"GLCHAT_CADDY_PROFILE=${CADDY_PROFILE}" \ "GLCHAT_CADDY_PROFILE=${CADDY_PROFILE}" \
"GLCHAT_TURN_CERT_DIR=${TURN_CERT_DIR}" \ "GLCHAT_TURN_CERT_DIR=${TURN_CERT_DIR}" \
"GLCHAT_APP_BIND=${APP_BIND_ADDR}" "GLCHAT_LIVEKIT_BIND=${LIVEKIT_BIND_ADDR}" \
"LIVEKIT_API_KEY=${LIVEKIT_API_KEY}" "LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET}" \ "LIVEKIT_API_KEY=${LIVEKIT_API_KEY}" "LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET}" \
"HTTP_PORT=${HTTP_PORT}" "HTTPS_PORT=${HTTPS_PORT}" "HTTP_PORT=${HTTP_PORT}" "HTTPS_PORT=${HTTPS_PORT}"
@@ -833,21 +890,42 @@ render_all_templates() {
# install_cert_refresh копирует обновлённый сертификат TURN и перезапускает LiveKit. # install_cert_refresh копирует обновлённый сертификат TURN и перезапускает LiveKit.
install_cert_refresh() { install_cert_refresh() {
[ "$TURN_ENABLED" = "true" ] || return 0 [ "$TURN_ENABLED" = "true" ] || return 0
[ -n "$TURN_CERT_SRC" ] || return 0 [ -n "$TURN_CERT_SRC" ] || [ -n "$TURN_CERT_REMOTE" ] || return 0
[ "$DRY_RUN" = "1" ] && return 0 [ "$DRY_RUN" = "1" ] && return 0
cat >/usr/local/sbin/glchat-cert-refresh <<EOF cat >/usr/local/sbin/glchat-cert-refresh <<EOF
#!/usr/bin/env bash #!/usr/bin/env bash
# Сгенерировано deploy/install.sh: копирует свежий сертификат TURN/TLS # Сгенерировано deploy/install.sh: копирует свежий сертификат TURN/TLS
# из внешнего ACME-хранилища и перезапускает LiveKit при изменении. # из внешнего ACME-хранилища (локально или по SSH) и перезапускает LiveKit
# при изменении.
set -eu set -eu
SRC_CRT="${TURN_CERT_SRC}" SRC_CRT="${TURN_CERT_SRC}"
SRC_KEY="${TURN_CERT_SRC_KEY}" SRC_KEY="${TURN_CERT_SRC_KEY}"
REMOTE="${TURN_CERT_REMOTE}"
REMOTE_CRT="${TURN_CERT_REMOTE_CRT}"
REMOTE_KEY="${TURN_CERT_REMOTE_KEY}"
SSH_KEY="${TURN_CERT_SSH_KEY}"
SSH_PORT="${TURN_CERT_SSH_PORT}"
DST_DIR="${TURN_CERT_DIR}" DST_DIR="${TURN_CERT_DIR}"
if [ ! -r "\$SRC_CRT" ] || [ ! -r "\$SRC_KEY" ]; then TMP_CRT="\$(mktemp)"; TMP_KEY="\$(mktemp)"
echo "glchat-cert-refresh: исходный сертификат недоступен" >&2 trap 'rm -f "\$TMP_CRT" "\$TMP_KEY"' EXIT
exit 1
if [ -n "\$REMOTE" ]; then
ssh_opts="-o BatchMode=yes -o StrictHostKeyChecking=accept-new -p \$SSH_PORT"
[ -n "\$SSH_KEY" ] && ssh_opts="\$ssh_opts -i \$SSH_KEY"
# shellcheck disable=SC2086
ssh \$ssh_opts "\$REMOTE" "cat '\$REMOTE_CRT'" >"\$TMP_CRT" || exit 1
# shellcheck disable=SC2086
ssh \$ssh_opts "\$REMOTE" "cat '\$REMOTE_KEY'" >"\$TMP_KEY" || exit 1
else
if [ ! -r "\$SRC_CRT" ] || [ ! -r "\$SRC_KEY" ]; then
echo "glchat-cert-refresh: исходный сертификат недоступен" >&2
exit 1
fi
cp "\$SRC_CRT" "\$TMP_CRT"
cp "\$SRC_KEY" "\$TMP_KEY"
fi fi
SRC_CRT="\$TMP_CRT"; SRC_KEY="\$TMP_KEY"
before="\$(sha256sum "\$DST_DIR/turn.crt" 2>/dev/null | cut -d' ' -f1 || true)" before="\$(sha256sum "\$DST_DIR/turn.crt" 2>/dev/null | cut -d' ' -f1 || true)"
install -m 640 "\$SRC_CRT" "\$DST_DIR/turn.crt" install -m 640 "\$SRC_CRT" "\$DST_DIR/turn.crt"
install -m 640 "\$SRC_KEY" "\$DST_DIR/turn.key" install -m 640 "\$SRC_KEY" "\$DST_DIR/turn.key"