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 VERSION=dev
ARG COMMIT=none ARG COMMIT=none
ARG BUILD_DATE=unknown 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 ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY cmd/ ./cmd/ COPY cmd/ ./cmd/
+22 -8
View File
@@ -16,6 +16,10 @@ export GOMODCACHE:= $(CACHE_DIR)/go-mod
export GOPATH := $(CACHE_DIR)/gopath export GOPATH := $(CACHE_DIR)/gopath
export GOTOOLCHAIN := local export GOTOOLCHAIN := local
export GOLANGCI_LINT_CACHE := $(CACHE_DIR)/golangci-lint 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 export CGO_ENABLED ?= 1
GOFLAGS_LOCAL := -trimpath GOFLAGS_LOCAL := -trimpath
@@ -24,7 +28,10 @@ GO_LDFLAGS := -s -w \
-X main.commit=$(COMMIT) \ -X main.commit=$(COMMIT) \
-X main.buildDate=$(BUILD_DATE) -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) export PATH := $(BIN_DIR):$(PATH)
GO_PACKAGES := ./cmd/... ./internal/... GO_PACKAGES := ./cmd/... ./internal/...
@@ -43,10 +50,15 @@ help: ## Показать список целей
@printf '\nПрофиль: %s | Кэш: %s\n' "$${PROFILE:-auto}" "$(CACHE_DIR)" @printf '\nПрофиль: %s | Кэш: %s\n' "$${PROFILE:-auto}" "$(CACHE_DIR)"
.PHONY: tools .PHONY: tools
tools: $(TOOLS) ## Установить зафиксированные dev-инструменты в .cache/bin tools: ## Установить dev-инструменты в .cache/bin (уже установленные в системе пропускаются)
$(CACHE_DIR):
@mkdir -p $(CACHE_DIR) $(BIN_DIR) @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) $(BIN_DIR)/golangci-lint: | $(CACHE_DIR)
@echo "==> install golangci-lint v2.13.2" @echo "==> install golangci-lint v2.13.2"
@@ -54,11 +66,11 @@ $(BIN_DIR)/golangci-lint: | $(CACHE_DIR)
$(BIN_DIR)/gofumpt: | $(CACHE_DIR) $(BIN_DIR)/gofumpt: | $(CACHE_DIR)
@echo "==> install gofumpt" @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) $(BIN_DIR)/sqlc: | $(CACHE_DIR)
@echo "==> install sqlc" @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) $(BIN_DIR)/goose: | $(CACHE_DIR)
@echo "==> install goose" @echo "==> install goose"
@@ -66,11 +78,11 @@ $(BIN_DIR)/goose: | $(CACHE_DIR)
$(BIN_DIR)/govulncheck: | $(CACHE_DIR) $(BIN_DIR)/govulncheck: | $(CACHE_DIR)
@echo "==> install govulncheck" @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) $(BIN_DIR)/gitleaks: | $(CACHE_DIR)
@echo "==> install gitleaks" @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 .PHONY: dev
dev: ## Запустить сервер локально (данные в ./build/dev-data) dev: ## Запустить сервер локально (данные в ./build/dev-data)
@@ -166,6 +178,8 @@ docker-build: ## Собрать образ приложения
--build-arg VERSION=$(VERSION) \ --build-arg VERSION=$(VERSION) \
--build-arg COMMIT=$(COMMIT) \ --build-arg COMMIT=$(COMMIT) \
--build-arg BUILD_DATE=$(BUILD_DATE) \ --build-arg BUILD_DATE=$(BUILD_DATE) \
--build-arg GOPROXY=$(GOPROXY) \
--build-arg GOSUMDB=$(GOSUMDB) \
-t $(DOCKER_IMAGE):$(DOCKER_TAG) . -t $(DOCKER_IMAGE):$(DOCKER_TAG) .
.PHONY: install-dry-run .PHONY: install-dry-run
+4 -4
View File
@@ -1,7 +1,7 @@
# Сгенерировано deploy/install.sh — не редактируйте вручную. # Сгенерировано deploy/install.sh — не редактируйте вручную.
# Режим: {{PROXY_MODE}} | Домен: {{DOMAIN}} # Режим: ${PROXY_MODE} | Домен: ${DOMAIN}
{ {
email {{ACME_EMAIL}} email ${ACME_EMAIL}
admin off admin off
log { log {
output file /var/log/caddy/access.log { output file /var/log/caddy/access.log {
@@ -22,7 +22,7 @@
} }
} }
{{DOMAIN}} { ${DOMAIN} {
encode zstd gzip encode zstd gzip
import security_headers import security_headers
@@ -55,7 +55,7 @@
} }
} }
{{FILES_DOMAIN}} { ${FILES_DOMAIN} {
encode zstd gzip encode zstd gzip
import security_headers import security_headers
handle { handle {
+7 -7
View File
@@ -3,7 +3,7 @@
# блок ниже нужно вставить в конфиг вашего прокси (например, Caddyfile) и перезагрузить его. # блок ниже нужно вставить в конфиг вашего прокси (например, Caddyfile) и перезагрузить его.
# Приложение и LiveKit слушают только 127.0.0.1. # Приложение и LiveKit слушают только 127.0.0.1.
{ {
email {{ACME_EMAIL}} email ${ACME_EMAIL}
} }
(glchat_security_headers) { (glchat_security_headers) {
@@ -17,7 +17,7 @@
} }
# --- glchat: начало блока ------------------------------------------------- # --- glchat: начало блока -------------------------------------------------
{{DOMAIN}} { ${DOMAIN} {
encode zstd gzip encode zstd gzip
import glchat_security_headers import glchat_security_headers
log { log {
@@ -31,7 +31,7 @@
# WebSocket Gateway (фаза 1): длинные соединения без таймаутов. # WebSocket Gateway (фаза 1): длинные соединения без таймаутов.
@gateway path /gateway @gateway path /gateway
handle @gateway { handle @gateway {
reverse_proxy 127.0.0.1:{{APP_PORT}} { reverse_proxy 127.0.0.1:${APP_PORT} {
transport http { transport http {
read_timeout 1h read_timeout 1h
write_timeout 1h write_timeout 1h
@@ -42,7 +42,7 @@
# LiveKit сигналинг (WebSocket). # LiveKit сигналинг (WebSocket).
@rtc path /rtc* @rtc path /rtc*
handle @rtc { handle @rtc {
reverse_proxy 127.0.0.1:{{LIVEKIT_WS_PORT}} { reverse_proxy 127.0.0.1:${LIVEKIT_WS_PORT} {
transport http { transport http {
read_timeout 1h read_timeout 1h
write_timeout 1h write_timeout 1h
@@ -52,16 +52,16 @@
# API и статика клиента. # API и статика клиента.
handle { handle {
reverse_proxy 127.0.0.1:{{APP_PORT}} reverse_proxy 127.0.0.1:${APP_PORT}
} }
} }
# Пользовательский контент — отдельный origin (изоляция cookies/CSP). # Пользовательский контент — отдельный origin (изоляция cookies/CSP).
{{FILES_DOMAIN}} { ${FILES_DOMAIN} {
encode zstd gzip encode zstd gzip
import glchat_security_headers import glchat_security_headers
handle { handle {
reverse_proxy 127.0.0.1:{{APP_PORT}} reverse_proxy 127.0.0.1:${APP_PORT}
} }
} }
# --- glchat: конец блока -------------------------------------------------- # --- 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/install.sh — не редактируйте вручную, изменения перезапишутся.
# Шаблон: deploy/compose.yaml.tpl. Профиль: {{PROFILE}} # Шаблон: deploy/compose.yaml.tpl. Профиль: ${PROFILE}
name: glchat name: glchat
x-logging: &default-logging x-logging: &default-logging
@@ -9,24 +9,24 @@ x-logging: &default-logging
max-file: "3" max-file: "3"
x-app-env: &app-env x-app-env: &app-env
DOMAIN: "{{DOMAIN}}" DOMAIN: "${DOMAIN}"
FILES_DOMAIN: "{{FILES_DOMAIN}}" FILES_DOMAIN: "${FILES_DOMAIN}"
INSTANCE_NAME: "{{INSTANCE_NAME}}" INSTANCE_NAME: "${INSTANCE_NAME}"
LISTEN_ADDR: "0.0.0.0:8080" LISTEN_ADDR: "0.0.0.0:8080"
DATA_DIR: "/app/data" DATA_DIR: "/app/data"
WEB_ROOT: "/app/web" WEB_ROOT: "/app/web"
LOG_LEVEL: "{{LOG_LEVEL}}" LOG_LEVEL: "${LOG_LEVEL}"
LOG_FORMAT: "json" LOG_FORMAT: "json"
SQLITE_READ_POOL: "{{SQLITE_READ_POOL}}" SQLITE_READ_POOL: "${SQLITE_READ_POOL}"
SQLITE_CACHE_SIZE: "{{SQLITE_CACHE_SIZE}}" SQLITE_CACHE_SIZE: "${SQLITE_CACHE_SIZE}"
SQLITE_MMAP_SIZE: "{{SQLITE_MMAP_SIZE}}" SQLITE_MMAP_SIZE: "${SQLITE_MMAP_SIZE}"
GOMAXPROCS: "{{GOMAXPROCS}}" GOMAXPROCS: "${GOMAXPROCS}"
MAX_UPLOAD_SIZE: "{{MAX_UPLOAD_SIZE}}" MAX_UPLOAD_SIZE: "${MAX_UPLOAD_SIZE}"
SHUTDOWN_TIMEOUT_SECONDS: "20" SHUTDOWN_TIMEOUT_SECONDS: "20"
services: services:
app: app:
image: {{APP_IMAGE}} image: ${APP_IMAGE}
container_name: glchat-app container_name: glchat-app
restart: unless-stopped restart: unless-stopped
env_file: env_file:
@@ -34,17 +34,17 @@ services:
environment: environment:
<<: *app-env <<: *app-env
ports: ports:
- "127.0.0.1:{{APP_PORT}}:8080" - "127.0.0.1:${APP_PORT}:8080"
volumes: volumes:
- "{{DATA_DIR}}/db:/app/data/db" - "${DATA_DIR}/db:/app/data/db"
- "{{DATA_DIR}}/files:/app/data/files" - "${DATA_DIR}/files:/app/data/files"
- "{{DATA_DIR}}/backups:/app/data/backups" - "${DATA_DIR}/backups:/app/data/backups"
logging: *default-logging logging: *default-logging
deploy: deploy:
resources: resources:
limits: limits:
cpus: "{{APP_CPUS}}" cpus: "${APP_CPUS}"
memory: {{APP_MEMORY_LIMIT}} memory: ${APP_MEMORY_LIMIT}
healthcheck: healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/healthz"] test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/healthz"]
interval: 30s interval: 30s
@@ -57,21 +57,24 @@ services:
- ALL - ALL
livekit: livekit:
image: livekit/livekit-server:{{LIVEKIT_IMAGE_TAG}} image: livekit/livekit-server:${LIVEKIT_IMAGE_TAG}
container_name: glchat-livekit container_name: glchat-livekit
restart: unless-stopped restart: unless-stopped
command: ["--config", "/etc/livekit.yaml"] command: ["--config", "/etc/livekit.yaml"]
environment: environment:
LIVEKIT_KEYS: "{{LIVEKIT_API_KEY}}: {{LIVEKIT_API_SECRET}}" LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}"
ports:{{LIVEKIT_PORTS}} ports:
- "127.0.0.1:${LIVEKIT_WS_PORT}:7880"
- "${LIVEKIT_TCP_PORT}:7881"
- "${LIVEKIT_UDP_PORT}:7882/udp"
volumes: volumes:
- ./livekit/livekit.yaml:/etc/livekit.yaml:ro - ./livekit/livekit.yaml:/etc/livekit.yaml:ro
logging: *default-logging logging: *default-logging
deploy: deploy:
resources: resources:
limits: limits:
cpus: "{{LIVEKIT_CPUS}}" cpus: "${LIVEKIT_CPUS}"
memory: {{LIVEKIT_MEMORY_LIMIT}} memory: ${LIVEKIT_MEMORY_LIMIT}
healthcheck: healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:7880"] test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:7880"]
interval: 30s interval: 30s
@@ -86,14 +89,14 @@ services:
- NET_BIND_SERVICE - NET_BIND_SERVICE
caddy: caddy:
image: caddy:{{CADDY_IMAGE_TAG}} image: caddy:${CADDY_IMAGE_TAG}
container_name: glchat-caddy container_name: glchat-caddy
restart: unless-stopped restart: unless-stopped
profiles: ["{{CADDY_PROFILE}}"] profiles: ["${CADDY_PROFILE}"]
ports: ports:
- "{{HTTP_PORT}}:80" - "${HTTP_PORT}:80"
- "{{HTTPS_PORT}}:443" - "${HTTPS_PORT}:443"
- "{{HTTPS_PORT}}:443/udp" - "${HTTPS_PORT}:443/udp"
volumes: volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy/data:/data - ./caddy/data:/data
@@ -103,8 +106,8 @@ services:
deploy: deploy:
resources: resources:
limits: limits:
cpus: "{{CADDY_CPUS}}" cpus: "${CADDY_CPUS}"
memory: {{CADDY_MEMORY_LIMIT}} memory: ${CADDY_MEMORY_LIMIT}
healthcheck: healthcheck:
test: ["CMD", "caddy", "version"] test: ["CMD", "caddy", "version"]
interval: 30s interval: 30s
+1 -1
View File
@@ -11,7 +11,7 @@ action = iptables-allports[name=glchat-auth]
[sshd] [sshd]
enabled = true enabled = true
port = {{SSH_PORT}} port = ${SSH_PORT}
maxretry = 5 maxretry = 5
findtime = 10m findtime = 10m
bantime = 1h bantime = 1h
+25 -19
View File
@@ -193,6 +193,10 @@ interactive_defaults() {
detect_ssh_port() { detect_ssh_port() {
local port="22" conf="/etc/ssh/sshd_config" 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 if [ -r "$conf" ]; then
local found local found
found="$(awk '/^[[:space:]]*Port[[:space:]]+[0-9]+/ {print $2; exit}' "$conf")" found="$(awk '/^[[:space:]]*Port[[:space:]]+[0-9]+/ {print $2; exit}' "$conf")"
@@ -547,12 +551,14 @@ ensure_swap() {
ok "swap ${want_gb} GB активирован" ok "swap ${want_gb} GB активирован"
} }
render_all_templates() { render_all_templates() {
info "генерируем конфиги в ${TARGET_DIR}" info "генерируем конфиги в ${TARGET_DIR}"
mkdir -p "${TARGET_DIR}"/{caddy/logs,caddy/data,caddy/config,livekit,fail2ban,nftables}
if [ "$DRY_RUN" = "1" ]; then if [ "$DRY_RUN" = "1" ]; then
log_file_only "DRY-RUN: рендер шаблонов compose/Caddy/LiveKit/fail2ban/nftables/systemd" log_file_only "DRY-RUN: рендер шаблонов compose/Caddy/LiveKit/fail2ban/nftables/systemd"
else 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}/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}/"*.sh "${TARGET_DIR}/" 2>/dev/null || true
cp "${DEPLOY_DIR}/fail2ban/filter-glchat-auth.conf" "${TARGET_DIR}/fail2ban/" cp "${DEPLOY_DIR}/fail2ban/filter-glchat-auth.conf" "${TARGET_DIR}/fail2ban/"
@@ -575,20 +581,14 @@ render_all_templates() {
LIVEKIT_PUBLISH="true" LIVEKIT_PUBLISH="true"
[ "$SKIP_TLS" = "1" ] && LIVEKIT_PUBLISH="false" [ "$SKIP_TLS" = "1" ] && LIVEKIT_PUBLISH="false"
# Публикация портов LiveKit отключается в тестовом режиме (--skip-tls), # Вариант compose выбирается по режиму: в тестовом прогоне (--skip-tls)
# когда установщик проверяется внутри контейнера. # порты LiveKit не публикуются, чтобы не занимать порты хост-машины.
local livekit_ports local compose_template="${DEPLOY_DIR}/compose.yaml.tpl"
if [ "$LIVEKIT_PUBLISH" = "true" ]; then if [ "$LIVEKIT_PUBLISH" != "true" ]; then
livekit_ports=" compose_template="${DEPLOY_DIR}/compose.test.yaml.tpl"
- \"127.0.0.1:${LIVEKIT_WS_PORT}:7880\"
- \"${LIVEKIT_TCP_PORT}:7881\"
- \"${LIVEKIT_UDP_PORT}:7882/udp\""
else
livekit_ports=" []"
fi 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}" \ "DOMAIN=${DOMAIN}" "FILES_DOMAIN=${FILES_DOMAIN_RESOLVED}" "INSTANCE_NAME=${INSTANCE_NAME}" \
"PROFILE=${PROFILE_RESOLVED}" "APP_IMAGE=${APP_IMAGE}" "IMAGE_TAG=${IMAGE_TAG}" \ "PROFILE=${PROFILE_RESOLVED}" "APP_IMAGE=${APP_IMAGE}" "IMAGE_TAG=${IMAGE_TAG}" \
"APP_PORT=${APP_PORT}" "LIVEKIT_WS_PORT=${LIVEKIT_WS_PORT}" \ "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_CPUS=1.0" "CADDY_MEMORY_LIMIT=256m" \
"CADDY_IMAGE_TAG=${CADDY_IMAGE_TAG}" "LIVEKIT_IMAGE_TAG=${LIVEKIT_IMAGE_TAG}" \ "CADDY_IMAGE_TAG=${CADDY_IMAGE_TAG}" "LIVEKIT_IMAGE_TAG=${LIVEKIT_IMAGE_TAG}" \
"CADDY_PROFILE=${CADDY_PROFILE}" "LIVEKIT_PUBLISH=${LIVEKIT_PUBLISH}" \ "CADDY_PROFILE=${CADDY_PROFILE}" "LIVEKIT_PUBLISH=${LIVEKIT_PUBLISH}" \
"LIVEKIT_PORTS=${LIVEKIT_PORTS}" \
"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}"
@@ -632,10 +631,15 @@ render_all_templates() {
TURN_EXTERNAL_TLS="$turn_external_tls" TURN_EXTERNAL_TLS="$turn_external_tls"
render_template "${DEPLOY_DIR}/livekit/livekit.yaml.tpl" "${TARGET_DIR}/livekit/livekit.yaml" \ 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}" \ "LIVEKIT_USE_EXTERNAL_IP=${use_external_ip}" \
"TURN_ENABLED=${TURN_ENABLED}" "TURN_DOMAIN=turn.${DOMAIN}" \ "TURN_ENABLED=${TURN_ENABLED}" \
"TURN_UDP_PORT=3478" "TURN_TLS_PORT=5349" "TURN_EXTERNAL_TLS=${turn_external_tls}" \ "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_MAX_PARTICIPANTS=${PROFILE_LIVEKIT_MAX_PARTICIPANTS}" \
"LIVEKIT_DISABLE_SIMULCAST=${disable_simulcast}" "LIVEKIT_DISABLE_SIMULCAST=${disable_simulcast}"
@@ -872,6 +876,8 @@ build_or_pull_image() {
--build-arg "VERSION=${IMAGE_TAG}" \ --build-arg "VERSION=${IMAGE_TAG}" \
--build-arg "COMMIT=$(git -C "$REPO_DIR" rev-parse --short HEAD 2>/dev/null || echo none)" \ --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 "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 "сборка образа не удалась" "$REPO_DIR" || die "сборка образа не удалась"
ok "образ собран: $(docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}' "$APP_IMAGE" | head -1)" ok "образ собран: $(docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}' "$APP_IMAGE" | head -1)"
} }
@@ -955,7 +961,7 @@ print_summary() {
* Не забудьте про внешний firewall провайдера (если есть). * Не забудьте про внешний firewall провайдера (если есть).
EOF EOF
if [ "$SKIP_TLS" = "0" ] && [ "$PROXY_MODE" = "embedded" ]; then if [ "$SKIP_TLS" = "0" ] && [ "$PROXY_MODE" = "embedded" ]; then
printf ' * Сертификат Let%s Encrypt выпускается автоматически при первом запросе.\n' "'" printf " * Сертификат Let's Encrypt выпускается автоматически при первом запросе.\n"
fi fi
printf '\n' printf '\n'
} }
@@ -989,7 +995,7 @@ main() {
else else
EFFECTIVE_UPLOAD_MB="$MAX_UPLOAD_MB" EFFECTIVE_UPLOAD_MB="$MAX_UPLOAD_MB"
fi fi
PROFILE_APP_CPUS="$(nproc)" PROFILE_APP_CPUS="$(detect_cpus)"
PROFILE_LIVEKIT_CPUS="1.0" PROFILE_LIVEKIT_CPUS="1.0"
PROFILE_SWAPPINESS=10 PROFILE_SWAPPINESS=10
+66 -9
View File
@@ -41,6 +41,11 @@ die() {
} }
need_root() { need_root() {
# В режиме --dry-run изменения не применяются, поэтому root не требуется:
# это позволяет проверять установщик на машине разработчика.
if [ "${DRY_RUN:-0}" = "1" ]; then
return 0
fi
if [ "$(id -u)" -ne 0 ]; then if [ "$(id -u)" -ne 0 ]; then
die "запустите установщик от root: sudo bash deploy/install.sh" die "запустите установщик от root: sudo bash deploy/install.sh"
fi 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 PROFILE_SWAP_GB=2 PROFILE_BACKUP_KEEP=30 PROFILE_SHARE_SOFT=4K/30 PROFILE_SHARE_MAX=4K/60
;; ;;
pro) pro)
PROFILE_SQLITE_CACHE_MB=64 PROFILE_SQLITE_MMAP_MB=1024 PROFILE_SQLITE_READ_POOL="$(nproc)" PROFILE_SQLITE_CACHE_MB=64 PROFILE_SQLITE_MMAP_MB=1024 PROFILE_SQLITE_READ_POOL="$(detect_cpus)"
PROFILE_GOMAXPROCS="$(nproc)" PROFILE_LRU_ENTRIES=200000 PROFILE_GOMAXPROCS="$(detect_cpus)" PROFILE_LRU_ENTRIES=200000
PROFILE_LIVEKIT_MAX_PARTICIPANTS=100 PROFILE_LIVEKIT_MAX_VIDEO=32 PROFILE_LIVEKIT_SIMULCAST=3 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_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 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() { detect_profile() {
local cpus ram_mb local cpus ram_mb
cpus="$(nproc)" cpus="$(detect_cpus)"
if [ -r /proc/meminfo ]; then
ram_mb="$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo)" ram_mb="$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo)"
else
ram_mb=4096
fi
if [ "$cpus" -le 1 ] && [ "$ram_mb" -lt 3072 ]; then if [ "$cpus" -le 1 ] && [ "$ram_mb" -lt 3072 ]; then
printf 'lite' printf 'lite'
elif [ "$cpus" -le 2 ] || [ "$ram_mb" -lt 5120 ]; then 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() { render_template() {
local src="$1" dst="$2" local src="$1" dst="$2"
shift 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 local content
if command -v envsubst >/dev/null 2>&1; then
content="$(envsubst <"$src")"
else
content="$(cat "$src")" content="$(cat "$src")"
while [ "$#" -gt 0 ]; do for pair in "$@"; do
local key="${1%%=*}" value="${1#*=}" local key="${pair%%=*}" value="${pair#*=}"
content="${content//"{{${key}}}"/$value}" content="${content//"{{${key}}}"/$value}"
done done
fi
if printf '%s' "$content" | grep -q '{{[A-Z_]\{2,\}}}'; then if printf '%s' "$content" | grep -q '{{[A-Z_]\{2,\}}}'; then
local leftover local leftover
leftover="$(printf '%s' "$content" | grep -o '{{[A-Z_]\{2,\}}}' | sort -u | tr '\n' ' ')" leftover="$(printf '%s' "$content" | grep -o '{{[A-Z_]\{2,\}}}' | sort -u | tr '\n' ' ')"
warn "в шаблоне $src остались незаполненные плейсхолдеры: $leftover" warn "в шаблоне $(basename "$src") остались незаполненные плейсхолдеры: $leftover"
fi fi
printf '%s\n' "$content" >"$dst" printf '%s\n' "$content" >"$dst"
} }
@@ -218,7 +253,14 @@ env_set() {
# --- общие проверки ------------------------------------------------------- # --- общие проверки -------------------------------------------------------
check_os() { 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 # shellcheck disable=SC1091
. /etc/os-release . /etc/os-release
case "${ID:-}" in case "${ID:-}" in
@@ -243,11 +285,15 @@ check_os() {
check_resources() { check_resources() {
local cpus ram_mb disk_mb 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)" ram_mb="$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo)"
else
ram_mb=0
fi
disk_mb="$(df -Pm / | awk 'NR==2 {print $4}')" disk_mb="$(df -Pm / | awk 'NR==2 {print $4}')"
info "ресурсы: ${cpus} vCPU, ${ram_mb} MB RAM, ${disk_mb} MB свободно на /" 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)" warn "меньше 2 GB RAM: профиль lite может не уложиться в бюджет (AGENT.md 9.1)"
fi fi
if [ "$disk_mb" -lt 8000 ]; then if [ "$disk_mb" -lt 8000 ]; then
@@ -255,6 +301,17 @@ check_resources() {
fi 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() { check_command() {
have "$1" && ok "найдено: $1" || warn "отсутствует: $1 (будет установлено)" have "$1" && ok "найдено: $1" || warn "отсутствует: $1 (будет установлено)"
} }
+11 -11
View File
@@ -1,29 +1,29 @@
# Сгенерировано deploy/install.sh — не редактируйте вручную. # Сгенерировано deploy/install.sh — не редактируйте вручную.
# Документация: https://docs.livekit.io/home/self-hosting/deployment/ # Документация: https://docs.livekit.io/home/self-hosting/deployment/
port: {{LIVEKIT_WS_PORT}} port: ${LIVEKIT_WS_PORT}
bind_addresses: bind_addresses:
- "0.0.0.0" - "0.0.0.0"
rtc: rtc:
tcp_port: {{LIVEKIT_TCP_PORT}} tcp_port: ${LIVEKIT_TCP_PORT}
udp_port: {{LIVEKIT_UDP_PORT}} udp_port: ${LIVEKIT_UDP_PORT}
use_external_ip: {{LIVEKIT_USE_EXTERNAL_IP}} use_external_ip: ${LIVEKIT_USE_EXTERNAL_IP}
# Синхронизация портов с публичным адресом: контейнер публикует порты напрямую. # Синхронизация портов с публичным адресом: контейнер публикует порты напрямую.
port_range_start: 0 port_range_start: 0
port_range_end: 0 port_range_end: 0
turn: turn:
enabled: {{TURN_ENABLED}} enabled: ${TURN_ENABLED}
domain: {{TURN_DOMAIN}} domain: ${TURN_DOMAIN}
udp_port: {{TURN_UDP_PORT}} udp_port: ${TURN_UDP_PORT}
tls_port: {{TURN_TLS_PORT}} tls_port: ${TURN_TLS_PORT}
external_tls: {{TURN_EXTERNAL_TLS}} external_tls: ${TURN_EXTERNAL_TLS}
room: room:
auto_create: true auto_create: true
empty_timeout: 300 empty_timeout: 300
departure_timeout: 20 departure_timeout: 20
max_participants: {{LIVEKIT_MAX_PARTICIPANTS}} max_participants: ${LIVEKIT_MAX_PARTICIPANTS}
enable_remote_unmute: false enable_remote_unmute: false
audio: audio:
@@ -34,7 +34,7 @@ audio:
video: video:
dynacast: true dynacast: true
disable_simulcast: {{LIVEKIT_DISABLE_SIMULCAST}} disable_simulcast: ${LIVEKIT_DISABLE_SIMULCAST}
logging: logging:
level: info level: info
+6 -6
View File
@@ -1,10 +1,10 @@
# Сгенерировано deploy/install.sh — не редактируйте вручную. # Сгенерировано deploy/install.sh — не редактируйте вручную.
# Ограничения против флуда и перебора (AGENT.md 9.7). Один IP: не более {{CONNLIMIT}} соединений # Ограничения против флуда и перебора (AGENT.md 9.7). Один IP: не более ${CONNLIMIT} соединений
# к 80/443 и не более {{CONN_RATE}} новых соединений в секунду. # к 80/443 и не более ${CONN_RATE} новых соединений в секунду.
table inet glchat { table inet glchat {
set web_ports { set web_ports {
type inet_service type inet_service
elements = { {{WEB_PORTS}} } elements = { ${WEB_PORTS} }
} }
chain input_guard { chain input_guard {
@@ -14,12 +14,12 @@ table inet glchat {
ct state established,related accept 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-флуда. # Защита от 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 exec "$CONTAINER" mkdir -p /opt/glchat-src
docker cp "${REPO_DIR}/." "${CONTAINER}:/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" 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 \ cd /opt/glchat-src && GLCHAT_ETC_DIR=/tmp/dryrun \
bash deploy/install.sh --dry-run --domain gl.test.local --email admin@gl.test.local \ 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 --admin admin@gl.test.local --password "test-password-123" --yes
' ' </dev/null
log "2/4 install.sh (боевой прогон: TLS и публичные порты не трогаем)" run_install "2/4 install.sh (боевой прогон: TLS и публичные порты не трогаем)"
docker exec "$CONTAINER" bash -lc " run_install "3/4 идемпотентность: повторный запуск с --reconfigure" --reconfigure
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
"
log "4/4 проверки состояния" log "4/4 проверки состояния"
docker exec "$CONTAINER" bash -lc ' docker exec "$CONTAINER" bash -lc '