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:
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: конец блока --------------------------------------------------
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
+71
-14
@@ -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)"
|
||||
ram_mb="$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo)"
|
||||
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
|
||||
local content
|
||||
content="$(cat "$src")"
|
||||
while [ "$#" -gt 0 ]; do
|
||||
local key="${1%%=*}" value="${1#*=}"
|
||||
content="${content//"{{${key}}}"/$value}"
|
||||
# Нечётное число аргументов означает, что вызов неполный: сообщаем об этом,
|
||||
# но продолжаем — значения, которые всё же переданы, будут применены,
|
||||
# а незаполненные плейсхолдеры видны в предупреждении ниже.
|
||||
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")"
|
||||
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)"
|
||||
ram_mb="$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo)"
|
||||
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 (будет установлено)"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 '
|
||||
|
||||
Reference in New Issue
Block a user