fix(deploy): надёжный рендер шаблонов и устойчивый цикл подстановки

- render_template: значения передаются через окружение (envsubst), а не
  подставляются в variadic-аргументы — многострочные значения больше не
  ломают разбор, неполный список не уводит bash в бесконечный цикл
- export "$pair": bash сам разбирает KEY=value (было export "${pair}")
- шаблоны переведены на ${VAR}-синтаксис, добавлен compose.test.yaml.tpl
  для тестового режима установщика (--skip-tls без публикации портов)
- dry-run не требует root и работает вне Linux (проверка ОС пропускается)
- install.sh: учитываются --livekit-*-порты и --turn-*-порты в livekit.yaml,
  исправлено экранирование апострофа в итоговой сводке
- scripts/debug-install.sh: трассировка установщика в контейнере
This commit is contained in:
2026-09-19 19:26:26 +03:00
parent 2bf7101f2d
commit 99e0f72844
13 changed files with 355 additions and 116 deletions
+4
View File
@@ -14,6 +14,10 @@ WORKDIR /src
ARG VERSION=dev
ARG COMMIT=none
ARG BUILD_DATE=unknown
# Сети без доступа к storage.googleapis.com не могут использовать proxy.golang.org.
ARG GOPROXY=https://proxy.golang.org,direct
ARG GOSUMDB=sum.golang.org
ENV GOPROXY=${GOPROXY} GOSUMDB=${GOSUMDB} GOTOOLCHAIN=local
COPY go.mod go.sum ./
RUN go mod download
COPY cmd/ ./cmd/
+22 -8
View File
@@ -16,6 +16,10 @@ export GOMODCACHE:= $(CACHE_DIR)/go-mod
export GOPATH := $(CACHE_DIR)/gopath
export GOTOOLCHAIN := local
export GOLANGCI_LINT_CACHE := $(CACHE_DIR)/golangci-lint
# Сети без доступа к storage.googleapis.com (например, домашний сервер) не могут
# скачать модули через proxy.golang.org; goproxy.io работает как fallback.
export GOPROXY ?= https://proxy.golang.org,direct
export GOSUMDB ?= sum.golang.org
export CGO_ENABLED ?= 1
GOFLAGS_LOCAL := -trimpath
@@ -24,7 +28,10 @@ GO_LDFLAGS := -s -w \
-X main.commit=$(COMMIT) \
-X main.buildDate=$(BUILD_DATE)
TOOLS := $(BIN_DIR)/golangci-lint $(BIN_DIR)/gofumpt $(BIN_DIR)/sqlc $(BIN_DIR)/goose $(BIN_DIR)/govulncheck $(BIN_DIR)/gitleaks
# Инструмент берётся из .cache/bin; если он уже установлен в системе
# (PATH), повторная установка не нужна — это важно для CI и серверов,
# где сборка инструментов из исходников недоступна.
SYSTEM_TOOLS := golangci-lint gofumpt sqlc goose govulncheck gitleaks
export PATH := $(BIN_DIR):$(PATH)
GO_PACKAGES := ./cmd/... ./internal/...
@@ -43,10 +50,15 @@ help: ## Показать список целей
@printf '\nПрофиль: %s | Кэш: %s\n' "$${PROFILE:-auto}" "$(CACHE_DIR)"
.PHONY: tools
tools: $(TOOLS) ## Установить зафиксированные dev-инструменты в .cache/bin
$(CACHE_DIR):
tools: ## Установить dev-инструменты в .cache/bin (уже установленные в системе пропускаются)
@mkdir -p $(CACHE_DIR) $(BIN_DIR)
@for tool in $(SYSTEM_TOOLS); do \
if command -v $$tool >/dev/null 2>&1; then \
printf ' ok %-16s %s\n' "$$tool" "$$(command -v $$tool)"; \
else \
$(MAKE) --no-print-directory $(BIN_DIR)/$$tool || exit 1; \
fi; \
done
$(BIN_DIR)/golangci-lint: | $(CACHE_DIR)
@echo "==> install golangci-lint v2.13.2"
@@ -54,11 +66,11 @@ $(BIN_DIR)/golangci-lint: | $(CACHE_DIR)
$(BIN_DIR)/gofumpt: | $(CACHE_DIR)
@echo "==> install gofumpt"
@GOBIN=$(BIN_DIR) go install mvdan.cc/gofumpt@v0.11.0
@GOBIN=$(BIN_DIR) go install mvdan.cc/gofumpt@v0.12.0
$(BIN_DIR)/sqlc: | $(CACHE_DIR)
@echo "==> install sqlc"
@GOBIN=$(BIN_DIR) go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.30.0
@GOBIN=$(BIN_DIR) go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1
$(BIN_DIR)/goose: | $(CACHE_DIR)
@echo "==> install goose"
@@ -66,11 +78,11 @@ $(BIN_DIR)/goose: | $(CACHE_DIR)
$(BIN_DIR)/govulncheck: | $(CACHE_DIR)
@echo "==> install govulncheck"
@GOBIN=$(BIN_DIR) go install golang.org/x/vuln/cmd/govulncheck@latest
@GOBIN=$(BIN_DIR) go install golang.org/x/vuln/cmd/govulncheck@v1.1.4
$(BIN_DIR)/gitleaks: | $(CACHE_DIR)
@echo "==> install gitleaks"
@GOBIN=$(BIN_DIR) go install github.com/zricethezav/gitleaks/v8@latest
@GOBIN=$(BIN_DIR) go install github.com/zricethezav/gitleaks/v8@v8.30.0
.PHONY: dev
dev: ## Запустить сервер локально (данные в ./build/dev-data)
@@ -166,6 +178,8 @@ docker-build: ## Собрать образ приложения
--build-arg VERSION=$(VERSION) \
--build-arg COMMIT=$(COMMIT) \
--build-arg BUILD_DATE=$(BUILD_DATE) \
--build-arg GOPROXY=$(GOPROXY) \
--build-arg GOSUMDB=$(GOSUMDB) \
-t $(DOCKER_IMAGE):$(DOCKER_TAG) .
.PHONY: install-dry-run
+4 -4
View File
@@ -1,7 +1,7 @@
# Сгенерировано deploy/install.sh — не редактируйте вручную.
# Режим: {{PROXY_MODE}} | Домен: {{DOMAIN}}
# Режим: ${PROXY_MODE} | Домен: ${DOMAIN}
{
email {{ACME_EMAIL}}
email ${ACME_EMAIL}
admin off
log {
output file /var/log/caddy/access.log {
@@ -22,7 +22,7 @@
}
}
{{DOMAIN}} {
${DOMAIN} {
encode zstd gzip
import security_headers
@@ -55,7 +55,7 @@
}
}
{{FILES_DOMAIN}} {
${FILES_DOMAIN} {
encode zstd gzip
import security_headers
handle {
+7 -7
View File
@@ -3,7 +3,7 @@
# блок ниже нужно вставить в конфиг вашего прокси (например, Caddyfile) и перезагрузить его.
# Приложение и LiveKit слушают только 127.0.0.1.
{
email {{ACME_EMAIL}}
email ${ACME_EMAIL}
}
(glchat_security_headers) {
@@ -17,7 +17,7 @@
}
# --- glchat: начало блока -------------------------------------------------
{{DOMAIN}} {
${DOMAIN} {
encode zstd gzip
import glchat_security_headers
log {
@@ -31,7 +31,7 @@
# WebSocket Gateway (фаза 1): длинные соединения без таймаутов.
@gateway path /gateway
handle @gateway {
reverse_proxy 127.0.0.1:{{APP_PORT}} {
reverse_proxy 127.0.0.1:${APP_PORT} {
transport http {
read_timeout 1h
write_timeout 1h
@@ -42,7 +42,7 @@
# LiveKit сигналинг (WebSocket).
@rtc path /rtc*
handle @rtc {
reverse_proxy 127.0.0.1:{{LIVEKIT_WS_PORT}} {
reverse_proxy 127.0.0.1:${LIVEKIT_WS_PORT} {
transport http {
read_timeout 1h
write_timeout 1h
@@ -52,16 +52,16 @@
# API и статика клиента.
handle {
reverse_proxy 127.0.0.1:{{APP_PORT}}
reverse_proxy 127.0.0.1:${APP_PORT}
}
}
# Пользовательский контент — отдельный origin (изоляция cookies/CSP).
{{FILES_DOMAIN}} {
${FILES_DOMAIN} {
encode zstd gzip
import glchat_security_headers
handle {
reverse_proxy 127.0.0.1:{{APP_PORT}}
reverse_proxy 127.0.0.1:${APP_PORT}
}
}
# --- glchat: конец блока --------------------------------------------------
+116
View File
@@ -0,0 +1,116 @@
# Сгенерировано deploy/install.sh — не редактируйте вручную, изменения перезапишутся.
# Шаблон: deploy/compose.yaml.tpl. Профиль: ${PROFILE}
name: glchat
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
x-app-env: &app-env
DOMAIN: "${DOMAIN}"
FILES_DOMAIN: "${FILES_DOMAIN}"
INSTANCE_NAME: "${INSTANCE_NAME}"
LISTEN_ADDR: "0.0.0.0:8080"
DATA_DIR: "/app/data"
WEB_ROOT: "/app/web"
LOG_LEVEL: "${LOG_LEVEL}"
LOG_FORMAT: "json"
SQLITE_READ_POOL: "${SQLITE_READ_POOL}"
SQLITE_CACHE_SIZE: "${SQLITE_CACHE_SIZE}"
SQLITE_MMAP_SIZE: "${SQLITE_MMAP_SIZE}"
GOMAXPROCS: "${GOMAXPROCS}"
MAX_UPLOAD_SIZE: "${MAX_UPLOAD_SIZE}"
SHUTDOWN_TIMEOUT_SECONDS: "20"
services:
app:
image: ${APP_IMAGE}
container_name: glchat-app
restart: unless-stopped
env_file:
- .env
environment:
<<: *app-env
ports:
- "127.0.0.1:${APP_PORT}:8080"
volumes:
- "${DATA_DIR}/db:/app/data/db"
- "${DATA_DIR}/files:/app/data/files"
- "${DATA_DIR}/backups:/app/data/backups"
logging: *default-logging
deploy:
resources:
limits:
cpus: "${APP_CPUS}"
memory: ${APP_MEMORY_LIMIT}
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/healthz"]
interval: 30s
timeout: 5s
start_period: 20s
retries: 3
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
livekit:
image: livekit/livekit-server:${LIVEKIT_IMAGE_TAG}
container_name: glchat-livekit
restart: unless-stopped
command: ["--config", "/etc/livekit.yaml"]
environment:
LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}"
# Порты LiveKit не публикуются: тестовый режим --skip-tls (проверка установщика в контейнере).
ports: []
volumes:
- ./livekit/livekit.yaml:/etc/livekit.yaml:ro
logging: *default-logging
deploy:
resources:
limits:
cpus: "${LIVEKIT_CPUS}"
memory: ${LIVEKIT_MEMORY_LIMIT}
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:7880"]
interval: 30s
timeout: 5s
start_period: 15s
retries: 3
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
caddy:
image: caddy:${CADDY_IMAGE_TAG}
container_name: glchat-caddy
restart: unless-stopped
profiles: ["${CADDY_PROFILE}"]
ports:
- "${HTTP_PORT}:80"
- "${HTTPS_PORT}:443"
- "${HTTPS_PORT}:443/udp"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy/data:/data
- ./caddy/config:/config
- ./caddy/logs:/var/log/caddy
logging: *default-logging
deploy:
resources:
limits:
cpus: "${CADDY_CPUS}"
memory: ${CADDY_MEMORY_LIMIT}
healthcheck:
test: ["CMD", "caddy", "version"]
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
security_opt:
- no-new-privileges:true
+32 -29
View File
@@ -1,5 +1,5 @@
# Сгенерировано deploy/install.sh — не редактируйте вручную, изменения перезапишутся.
# Шаблон: deploy/compose.yaml.tpl. Профиль: {{PROFILE}}
# Шаблон: deploy/compose.yaml.tpl. Профиль: ${PROFILE}
name: glchat
x-logging: &default-logging
@@ -9,24 +9,24 @@ x-logging: &default-logging
max-file: "3"
x-app-env: &app-env
DOMAIN: "{{DOMAIN}}"
FILES_DOMAIN: "{{FILES_DOMAIN}}"
INSTANCE_NAME: "{{INSTANCE_NAME}}"
DOMAIN: "${DOMAIN}"
FILES_DOMAIN: "${FILES_DOMAIN}"
INSTANCE_NAME: "${INSTANCE_NAME}"
LISTEN_ADDR: "0.0.0.0:8080"
DATA_DIR: "/app/data"
WEB_ROOT: "/app/web"
LOG_LEVEL: "{{LOG_LEVEL}}"
LOG_LEVEL: "${LOG_LEVEL}"
LOG_FORMAT: "json"
SQLITE_READ_POOL: "{{SQLITE_READ_POOL}}"
SQLITE_CACHE_SIZE: "{{SQLITE_CACHE_SIZE}}"
SQLITE_MMAP_SIZE: "{{SQLITE_MMAP_SIZE}}"
GOMAXPROCS: "{{GOMAXPROCS}}"
MAX_UPLOAD_SIZE: "{{MAX_UPLOAD_SIZE}}"
SQLITE_READ_POOL: "${SQLITE_READ_POOL}"
SQLITE_CACHE_SIZE: "${SQLITE_CACHE_SIZE}"
SQLITE_MMAP_SIZE: "${SQLITE_MMAP_SIZE}"
GOMAXPROCS: "${GOMAXPROCS}"
MAX_UPLOAD_SIZE: "${MAX_UPLOAD_SIZE}"
SHUTDOWN_TIMEOUT_SECONDS: "20"
services:
app:
image: {{APP_IMAGE}}
image: ${APP_IMAGE}
container_name: glchat-app
restart: unless-stopped
env_file:
@@ -34,17 +34,17 @@ services:
environment:
<<: *app-env
ports:
- "127.0.0.1:{{APP_PORT}}:8080"
- "127.0.0.1:${APP_PORT}:8080"
volumes:
- "{{DATA_DIR}}/db:/app/data/db"
- "{{DATA_DIR}}/files:/app/data/files"
- "{{DATA_DIR}}/backups:/app/data/backups"
- "${DATA_DIR}/db:/app/data/db"
- "${DATA_DIR}/files:/app/data/files"
- "${DATA_DIR}/backups:/app/data/backups"
logging: *default-logging
deploy:
resources:
limits:
cpus: "{{APP_CPUS}}"
memory: {{APP_MEMORY_LIMIT}}
cpus: "${APP_CPUS}"
memory: ${APP_MEMORY_LIMIT}
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/healthz"]
interval: 30s
@@ -57,21 +57,24 @@ services:
- ALL
livekit:
image: livekit/livekit-server:{{LIVEKIT_IMAGE_TAG}}
image: livekit/livekit-server:${LIVEKIT_IMAGE_TAG}
container_name: glchat-livekit
restart: unless-stopped
command: ["--config", "/etc/livekit.yaml"]
environment:
LIVEKIT_KEYS: "{{LIVEKIT_API_KEY}}: {{LIVEKIT_API_SECRET}}"
ports:{{LIVEKIT_PORTS}}
LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}"
ports:
- "127.0.0.1:${LIVEKIT_WS_PORT}:7880"
- "${LIVEKIT_TCP_PORT}:7881"
- "${LIVEKIT_UDP_PORT}:7882/udp"
volumes:
- ./livekit/livekit.yaml:/etc/livekit.yaml:ro
logging: *default-logging
deploy:
resources:
limits:
cpus: "{{LIVEKIT_CPUS}}"
memory: {{LIVEKIT_MEMORY_LIMIT}}
cpus: "${LIVEKIT_CPUS}"
memory: ${LIVEKIT_MEMORY_LIMIT}
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:7880"]
interval: 30s
@@ -86,14 +89,14 @@ services:
- NET_BIND_SERVICE
caddy:
image: caddy:{{CADDY_IMAGE_TAG}}
image: caddy:${CADDY_IMAGE_TAG}
container_name: glchat-caddy
restart: unless-stopped
profiles: ["{{CADDY_PROFILE}}"]
profiles: ["${CADDY_PROFILE}"]
ports:
- "{{HTTP_PORT}}:80"
- "{{HTTPS_PORT}}:443"
- "{{HTTPS_PORT}}:443/udp"
- "${HTTP_PORT}:80"
- "${HTTPS_PORT}:443"
- "${HTTPS_PORT}:443/udp"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy/data:/data
@@ -103,8 +106,8 @@ services:
deploy:
resources:
limits:
cpus: "{{CADDY_CPUS}}"
memory: {{CADDY_MEMORY_LIMIT}}
cpus: "${CADDY_CPUS}"
memory: ${CADDY_MEMORY_LIMIT}
healthcheck:
test: ["CMD", "caddy", "version"]
interval: 30s
+1 -1
View File
@@ -11,7 +11,7 @@ action = iptables-allports[name=glchat-auth]
[sshd]
enabled = true
port = {{SSH_PORT}}
port = ${SSH_PORT}
maxretry = 5
findtime = 10m
bantime = 1h
+25 -19
View File
@@ -193,6 +193,10 @@ interactive_defaults() {
detect_ssh_port() {
local port="22" conf="/etc/ssh/sshd_config"
if [ ! -r "$conf" ] && [ ! -d /etc/ssh ]; then
printf '%s' "$port"
return 0
fi
if [ -r "$conf" ]; then
local found
found="$(awk '/^[[:space:]]*Port[[:space:]]+[0-9]+/ {print $2; exit}' "$conf")"
@@ -547,12 +551,14 @@ ensure_swap() {
ok "swap ${want_gb} GB активирован"
}
render_all_templates() {
info "генерируем конфиги в ${TARGET_DIR}"
mkdir -p "${TARGET_DIR}"/{caddy/logs,caddy/data,caddy/config,livekit,fail2ban,nftables}
if [ "$DRY_RUN" = "1" ]; then
log_file_only "DRY-RUN: рендер шаблонов compose/Caddy/LiveKit/fail2ban/nftables/systemd"
else
mkdir -p "${TARGET_DIR}"/{caddy/logs,caddy/data,caddy/config,livekit,fail2ban,nftables}
cp "${DEPLOY_DIR}/lib/common.sh" "${TARGET_DIR}/lib-common.sh" 2>/dev/null || true
cp "${DEPLOY_DIR}/"*.sh "${TARGET_DIR}/" 2>/dev/null || true
cp "${DEPLOY_DIR}/fail2ban/filter-glchat-auth.conf" "${TARGET_DIR}/fail2ban/"
@@ -575,20 +581,14 @@ render_all_templates() {
LIVEKIT_PUBLISH="true"
[ "$SKIP_TLS" = "1" ] && LIVEKIT_PUBLISH="false"
# Публикация портов LiveKit отключается в тестовом режиме (--skip-tls),
# когда установщик проверяется внутри контейнера.
local livekit_ports
if [ "$LIVEKIT_PUBLISH" = "true" ]; then
livekit_ports="
- \"127.0.0.1:${LIVEKIT_WS_PORT}:7880\"
- \"${LIVEKIT_TCP_PORT}:7881\"
- \"${LIVEKIT_UDP_PORT}:7882/udp\""
else
livekit_ports=" []"
# Вариант compose выбирается по режиму: в тестовом прогоне (--skip-tls)
# порты LiveKit не публикуются, чтобы не занимать порты хост-машины.
local compose_template="${DEPLOY_DIR}/compose.yaml.tpl"
if [ "$LIVEKIT_PUBLISH" != "true" ]; then
compose_template="${DEPLOY_DIR}/compose.test.yaml.tpl"
fi
LIVEKIT_PORTS="$livekit_ports"
render_template "${DEPLOY_DIR}/compose.yaml.tpl" "${TARGET_DIR}/compose.yaml" \
render_template "${compose_template}" "${TARGET_DIR}/compose.yaml" \
"DOMAIN=${DOMAIN}" "FILES_DOMAIN=${FILES_DOMAIN_RESOLVED}" "INSTANCE_NAME=${INSTANCE_NAME}" \
"PROFILE=${PROFILE_RESOLVED}" "APP_IMAGE=${APP_IMAGE}" "IMAGE_TAG=${IMAGE_TAG}" \
"APP_PORT=${APP_PORT}" "LIVEKIT_WS_PORT=${LIVEKIT_WS_PORT}" \
@@ -604,7 +604,6 @@ render_all_templates() {
"CADDY_CPUS=1.0" "CADDY_MEMORY_LIMIT=256m" \
"CADDY_IMAGE_TAG=${CADDY_IMAGE_TAG}" "LIVEKIT_IMAGE_TAG=${LIVEKIT_IMAGE_TAG}" \
"CADDY_PROFILE=${CADDY_PROFILE}" "LIVEKIT_PUBLISH=${LIVEKIT_PUBLISH}" \
"LIVEKIT_PORTS=${LIVEKIT_PORTS}" \
"LIVEKIT_API_KEY=${LIVEKIT_API_KEY}" "LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET}" \
"HTTP_PORT=${HTTP_PORT}" "HTTPS_PORT=${HTTPS_PORT}"
@@ -632,10 +631,15 @@ render_all_templates() {
TURN_EXTERNAL_TLS="$turn_external_tls"
render_template "${DEPLOY_DIR}/livekit/livekit.yaml.tpl" "${TARGET_DIR}/livekit/livekit.yaml" \
"LIVEKIT_WS_PORT=7880" "LIVEKIT_TCP_PORT=7881" "LIVEKIT_UDP_PORT=7882" \
"LIVEKIT_WS_PORT=${LIVEKIT_WS_PORT}" \
"LIVEKIT_TCP_PORT=${LIVEKIT_TCP_PORT}" \
"LIVEKIT_UDP_PORT=${LIVEKIT_UDP_PORT}" \
"LIVEKIT_USE_EXTERNAL_IP=${use_external_ip}" \
"TURN_ENABLED=${TURN_ENABLED}" "TURN_DOMAIN=turn.${DOMAIN}" \
"TURN_UDP_PORT=3478" "TURN_TLS_PORT=5349" "TURN_EXTERNAL_TLS=${turn_external_tls}" \
"TURN_ENABLED=${TURN_ENABLED}" \
"TURN_DOMAIN=turn.${DOMAIN}" \
"TURN_UDP_PORT=${TURN_UDP_PORT}" \
"TURN_TLS_PORT=${TURN_TLS_PORT}" \
"TURN_EXTERNAL_TLS=${turn_external_tls}" \
"LIVEKIT_MAX_PARTICIPANTS=${PROFILE_LIVEKIT_MAX_PARTICIPANTS}" \
"LIVEKIT_DISABLE_SIMULCAST=${disable_simulcast}"
@@ -872,6 +876,8 @@ build_or_pull_image() {
--build-arg "VERSION=${IMAGE_TAG}" \
--build-arg "COMMIT=$(git -C "$REPO_DIR" rev-parse --short HEAD 2>/dev/null || echo none)" \
--build-arg "BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--build-arg "GOPROXY=${GOPROXY:-https://proxy.golang.org,direct}" \
--build-arg "GOSUMDB=${GOSUMDB:-sum.golang.org}" \
"$REPO_DIR" || die "сборка образа не удалась"
ok "образ собран: $(docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}' "$APP_IMAGE" | head -1)"
}
@@ -955,7 +961,7 @@ print_summary() {
* Не забудьте про внешний firewall провайдера (если есть).
EOF
if [ "$SKIP_TLS" = "0" ] && [ "$PROXY_MODE" = "embedded" ]; then
printf ' * Сертификат Let%s Encrypt выпускается автоматически при первом запросе.\n' "'"
printf " * Сертификат Let's Encrypt выпускается автоматически при первом запросе.\n"
fi
printf '\n'
}
@@ -989,7 +995,7 @@ main() {
else
EFFECTIVE_UPLOAD_MB="$MAX_UPLOAD_MB"
fi
PROFILE_APP_CPUS="$(nproc)"
PROFILE_APP_CPUS="$(detect_cpus)"
PROFILE_LIVEKIT_CPUS="1.0"
PROFILE_SWAPPINESS=10
+66 -9
View File
@@ -41,6 +41,11 @@ die() {
}
need_root() {
# В режиме --dry-run изменения не применяются, поэтому root не требуется:
# это позволяет проверять установщик на машине разработчика.
if [ "${DRY_RUN:-0}" = "1" ]; then
return 0
fi
if [ "$(id -u)" -ne 0 ]; then
die "запустите установщик от root: sudo bash deploy/install.sh"
fi
@@ -145,8 +150,8 @@ profile_values() {
PROFILE_SWAP_GB=2 PROFILE_BACKUP_KEEP=30 PROFILE_SHARE_SOFT=4K/30 PROFILE_SHARE_MAX=4K/60
;;
pro)
PROFILE_SQLITE_CACHE_MB=64 PROFILE_SQLITE_MMAP_MB=1024 PROFILE_SQLITE_READ_POOL="$(nproc)"
PROFILE_GOMAXPROCS="$(nproc)" PROFILE_LRU_ENTRIES=200000
PROFILE_SQLITE_CACHE_MB=64 PROFILE_SQLITE_MMAP_MB=1024 PROFILE_SQLITE_READ_POOL="$(detect_cpus)"
PROFILE_GOMAXPROCS="$(detect_cpus)" PROFILE_LRU_ENTRIES=200000
PROFILE_LIVEKIT_MAX_PARTICIPANTS=100 PROFILE_LIVEKIT_MAX_VIDEO=32 PROFILE_LIVEKIT_SIMULCAST=3
PROFILE_UPLOAD_MB=250 PROFILE_APP_LIMIT=4g PROFILE_LIVEKIT_LIMIT=6g
PROFILE_SWAP_GB=0 PROFILE_BACKUP_KEEP=60 PROFILE_SHARE_SOFT=4K/60 PROFILE_SHARE_MAX=4K/60
@@ -164,8 +169,12 @@ profile_values() {
detect_profile() {
local cpus ram_mb
cpus="$(nproc)"
cpus="$(detect_cpus)"
if [ -r /proc/meminfo ]; then
ram_mb="$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo)"
else
ram_mb=4096
fi
if [ "$cpus" -le 1 ] && [ "$ram_mb" -lt 3072 ]; then
printf 'lite'
elif [ "$cpus" -le 2 ] || [ "$ram_mb" -lt 5120 ]; then
@@ -179,19 +188,45 @@ detect_profile() {
# --- рендеринг шаблонов ---------------------------------------------------
# render_template src dst KEY=value...
#
# Значения передаются через окружение, а не подставляются в аргументы: это
# исключает разбор значений с пробелами и переносами строк и делает рендер
# предсказуемым (bash ${var//pat/repl} на многострочных значениях и variadic
# аргументах ведёт себя неочевидно). Если gettext-base (envsubst) недоступен,
# используется эквивалентная замена средствами bash.
render_template() {
local src="$1" dst="$2"
shift 2
# Нечётное число аргументов означает, что вызов неполный: сообщаем об этом,
# но продолжаем — значения, которые всё же переданы, будут применены,
# а незаполненные плейсхолдеры видны в предупреждении ниже.
if [ $(( $# % 2 )) -ne 0 ]; then
warn "render_template($(basename "$src")): нечётное число аргументов: $#"
fi
# export "$pair" разбирает KEY=value сам; export "${pair}" трактует всю
# пару как имя переменной — из-за этого значения не попадали в окружение.
local pair
for pair in "$@"; do
export "$pair"
done
local content
if command -v envsubst >/dev/null 2>&1; then
content="$(envsubst <"$src")"
else
content="$(cat "$src")"
while [ "$#" -gt 0 ]; do
local key="${1%%=*}" value="${1#*=}"
for pair in "$@"; do
local key="${pair%%=*}" value="${pair#*=}"
content="${content//"{{${key}}}"/$value}"
done
fi
if printf '%s' "$content" | grep -q '{{[A-Z_]\{2,\}}}'; then
local leftover
leftover="$(printf '%s' "$content" | grep -o '{{[A-Z_]\{2,\}}}' | sort -u | tr '\n' ' ')"
warn "в шаблоне $src остались незаполненные плейсхолдеры: $leftover"
warn "в шаблоне $(basename "$src") остались незаполненные плейсхолдеры: $leftover"
fi
printf '%s\n' "$content" >"$dst"
}
@@ -218,7 +253,14 @@ env_set() {
# --- общие проверки -------------------------------------------------------
check_os() {
[ -r /etc/os-release ] || die "не удалось прочитать /etc/os-release"
if [ ! -r /etc/os-release ]; then
# macOS и другие не-Linux системы: допустимо только для --dry-run.
if [ "${DRY_RUN:-0}" = "1" ]; then
warn "не Linux (нет /etc/os-release): проверка ОС пропущена в режиме --dry-run"
return 0
fi
die "не удалось прочитать /etc/os-release"
fi
# shellcheck disable=SC1091
. /etc/os-release
case "${ID:-}" in
@@ -243,11 +285,15 @@ check_os() {
check_resources() {
local cpus ram_mb disk_mb
cpus="$(nproc)"
cpus="$(detect_cpus)"
if [ -r /proc/meminfo ]; then
ram_mb="$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo)"
else
ram_mb=0
fi
disk_mb="$(df -Pm / | awk 'NR==2 {print $4}')"
info "ресурсы: ${cpus} vCPU, ${ram_mb} MB RAM, ${disk_mb} MB свободно на /"
if [ "$ram_mb" -lt 1800 ]; then
if [ "$ram_mb" -gt 0 ] && [ "$ram_mb" -lt 1800 ]; then
warn "меньше 2 GB RAM: профиль lite может не уложиться в бюджет (AGENT.md 9.1)"
fi
if [ "$disk_mb" -lt 8000 ]; then
@@ -255,6 +301,17 @@ check_resources() {
fi
}
detect_cpus() {
if command -v nproc >/dev/null 2>&1; then
nproc
return
fi
if command -v sysctl >/dev/null 2>&1; then
sysctl -n hw.ncpu 2>/dev/null && return
fi
printf '2'
}
check_command() {
have "$1" && ok "найдено: $1" || warn "отсутствует: $1 (будет установлено)"
}
+11 -11
View File
@@ -1,29 +1,29 @@
# Сгенерировано deploy/install.sh — не редактируйте вручную.
# Документация: https://docs.livekit.io/home/self-hosting/deployment/
port: {{LIVEKIT_WS_PORT}}
port: ${LIVEKIT_WS_PORT}
bind_addresses:
- "0.0.0.0"
rtc:
tcp_port: {{LIVEKIT_TCP_PORT}}
udp_port: {{LIVEKIT_UDP_PORT}}
use_external_ip: {{LIVEKIT_USE_EXTERNAL_IP}}
tcp_port: ${LIVEKIT_TCP_PORT}
udp_port: ${LIVEKIT_UDP_PORT}
use_external_ip: ${LIVEKIT_USE_EXTERNAL_IP}
# Синхронизация портов с публичным адресом: контейнер публикует порты напрямую.
port_range_start: 0
port_range_end: 0
turn:
enabled: {{TURN_ENABLED}}
domain: {{TURN_DOMAIN}}
udp_port: {{TURN_UDP_PORT}}
tls_port: {{TURN_TLS_PORT}}
external_tls: {{TURN_EXTERNAL_TLS}}
enabled: ${TURN_ENABLED}
domain: ${TURN_DOMAIN}
udp_port: ${TURN_UDP_PORT}
tls_port: ${TURN_TLS_PORT}
external_tls: ${TURN_EXTERNAL_TLS}
room:
auto_create: true
empty_timeout: 300
departure_timeout: 20
max_participants: {{LIVEKIT_MAX_PARTICIPANTS}}
max_participants: ${LIVEKIT_MAX_PARTICIPANTS}
enable_remote_unmute: false
audio:
@@ -34,7 +34,7 @@ audio:
video:
dynacast: true
disable_simulcast: {{LIVEKIT_DISABLE_SIMULCAST}}
disable_simulcast: ${LIVEKIT_DISABLE_SIMULCAST}
logging:
level: info
+6 -6
View File
@@ -1,10 +1,10 @@
# Сгенерировано deploy/install.sh — не редактируйте вручную.
# Ограничения против флуда и перебора (AGENT.md 9.7). Один IP: не более {{CONNLIMIT}} соединений
# к 80/443 и не более {{CONN_RATE}} новых соединений в секунду.
# Ограничения против флуда и перебора (AGENT.md 9.7). Один IP: не более ${CONNLIMIT} соединений
# к 80/443 и не более ${CONN_RATE} новых соединений в секунду.
table inet glchat {
set web_ports {
type inet_service
elements = { {{WEB_PORTS}} }
elements = { ${WEB_PORTS} }
}
chain input_guard {
@@ -14,12 +14,12 @@ table inet glchat {
ct state established,related accept
# Ограничение одновременных соединений с одного источника.
tcp dport @web_ports ct state new meter glchat_connlimit size 65535 { ip saddr limit over {{CONNLIMIT}} } drop
tcp dport @web_ports ct state new meter glchat_connlimit size 65535 { ip saddr limit over ${CONNLIMIT} } drop
# Ограничение скорости появления новых соединений.
tcp dport @web_ports ct state new meter glchat_rate size 65535 { ip saddr limit rate over {{CONN_RATE}}/second } drop
tcp dport @web_ports ct state new meter glchat_rate size 65535 { ip saddr limit rate over ${CONN_RATE}/second } drop
# Защита от SYN-флуда.
tcp flags & (fin|syn|rst|ack) == syn limit rate over {{SYN_RATE}}/second drop
tcp flags & (fin|syn|rst|ack) == syn limit rate over ${SYN_RATE}/second drop
}
}
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Диагностика зависания install.sh: трассировка пишется в файл, затем показывается хвост.
set -uo pipefail
CONTAINER="glchat-dbg-$$"
IMAGE="jrei/systemd-ubuntu:24.04"
cleanup() { docker rm -f "$CONTAINER" >/dev/null 2>&1 || true; }
trap cleanup EXIT
docker run -d --name "$CONTAINER" --privileged --cgroupns=host \
--tmpfs /tmp:exec,size=2g --tmpfs /run --tmpfs /run/lock \
-v /sys/fs/cgroup:/sys/fs/cgroup:rw "$IMAGE" /sbin/init >/dev/null
for _ in $(seq 1 30); do
docker exec "$CONTAINER" systemctl is-system-running 2>/dev/null | grep -qE 'running|degraded' && break
sleep 1
done
docker exec "$CONTAINER" mkdir -p /opt/glchat-src
docker cp /opt/glchat-src/. "$CONTAINER:/opt/glchat-src/"
echo "=== dry-run с трассировкой, лог в /tmp/trace.log ==="
timeout 60 docker exec -i "$CONTAINER" bash -lc '
cd /opt/glchat-src && PS4="+ \${BASH_SOURCE##*/}:\${LINENO}: " bash -x deploy/install.sh \
--dry-run --domain gl.test.local --email admin@gl.test.local \
--admin admin@gl.test.local --password "test-password-123" --yes > /tmp/trace.log 2>&1
' </dev/null
echo "exit=$?"
echo "=== последние 30 строк трассировки ==="
docker exec "$CONTAINER" bash -lc 'tail -30 /tmp/trace.log'
echo "=== функция, где остановилось ==="
docker exec "$CONTAINER" bash -lc 'grep -oE "install\.sh:[0-9]+" /tmp/trace.log | tail -1'
docker exec "$CONTAINER" bash -lc 'last=$(grep -oE "install\.sh:[0-9]+" /tmp/trace.log | tail -1 | cut -d: -f2); sed -n "$((last-5)),$((last+15))p" /opt/glchat-src/deploy/install.sh'
+21 -17
View File
@@ -37,28 +37,32 @@ log "копируем репозиторий внутрь контейнера"
docker exec "$CONTAINER" mkdir -p /opt/glchat-src
docker cp "${REPO_DIR}/." "${CONTAINER}:/opt/glchat-src/"
# stdin закрыт: иначе установщик может ждать интерактивного подтверждения.
run_install() {
local label="$1"
shift
log "$label"
if ! timeout 1500 docker exec -i "$CONTAINER" bash -lc "
cd /opt/glchat-src && GLCHAT_ETC_DIR=/opt/glchat \
bash deploy/install.sh --domain gl.test.local --email admin@gl.test.local \
--admin admin@gl.test.local --password 'test-password-123' \
--skip-tls --app-port 8080 --no-turn --yes $*
" </dev/null; then
echo "install.sh завершился с ошибкой (${label})" >&2
docker exec "$CONTAINER" bash -lc 'tail -40 /opt/glchat/install.log 2>/dev/null || true' || true
exit 1
fi
}
log "1/4 install.sh --dry-run"
docker exec "$CONTAINER" bash -lc '
timeout 600 docker exec -i "$CONTAINER" bash -lc '
cd /opt/glchat-src && GLCHAT_ETC_DIR=/tmp/dryrun \
bash deploy/install.sh --dry-run --domain gl.test.local --email admin@gl.test.local \
--admin admin@gl.test.local --password "test-password-123" --yes
'
' </dev/null
log "2/4 install.sh (боевой прогон: TLS и публичные порты не трогаем)"
docker exec "$CONTAINER" bash -lc "
cd /opt/glchat-src && GLCHAT_ETC_DIR=/opt/glchat \
bash deploy/install.sh --domain gl.test.local --email admin@gl.test.local \
--admin admin@gl.test.local --password 'test-password-123' \
--skip-tls --app-port 8080 --no-turn --yes
"
log "3/4 идемпотентность: повторный запуск с --reconfigure"
docker exec "$CONTAINER" bash -lc "
cd /opt/glchat-src && GLCHAT_ETC_DIR=/opt/glchat \
bash deploy/install.sh --domain gl.test.local --email admin@gl.test.local \
--admin admin@gl.test.local --password 'test-password-123' \
--skip-tls --app-port 8080 --no-turn --reconfigure --yes
"
run_install "2/4 install.sh (боевой прогон: TLS и публичные порты не трогаем)"
run_install "3/4 идемпотентность: повторный запуск с --reconfigure" --reconfigure
log "4/4 проверки состояния"
docker exec "$CONTAINER" bash -lc '