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 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/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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: конец блока --------------------------------------------------
|
||||||
|
|||||||
@@ -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/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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 (будет установлено)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 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 '
|
||||||
|
|||||||
Reference in New Issue
Block a user