fix(deploy): .env генерируется через подстановку @KEY@, а не через heredoc

Условия и подстановки внутри heredoc выполнялись shell при генерации файла
и попадали в .env как код — docker compose не мог его прочитать.
Теперь шаблон пишется с quoted heredoc, значения подставляются sed.
This commit is contained in:
2026-09-19 20:31:54 +03:00
parent 4b3a90ad7f
commit 0497016c69
+165 -79
View File
@@ -588,107 +588,193 @@ write_env_file() {
log_file_only "DRY-RUN: .env не записывается" log_file_only "DRY-RUN: .env не записывается"
return 0 return 0
fi fi
# Значения подставляются через sed, а не через расширение heredoc: иначе
# shell выполняет условия и подстановки прямо в тексте файла (уже ломало .env).
local tmp local tmp
tmp="$(mktemp)" tmp="$(mktemp)"
cat >"$tmp" <<EOF cat >"$tmp" <<'ENVEOF'
# glchat — окружение инстанса. Сгенерировано deploy/install.sh. # glchat — окружение инстанса. Сгенерировано deploy/install.sh.
# Секреты: не коммитить, не публиковать, не пересылать. Права: 600. # Секреты: не коммитить, не публиковать, не пересылать. Права: 600.
# --- инстанс --- # --- инстанс ---
DOMAIN=${DOMAIN} DOMAIN=@DOMAIN@
FILES_DOMAIN=${FILES_DOMAIN_RESOLVED} FILES_DOMAIN=@FILES_DOMAIN@
INSTANCE_NAME=${INSTANCE_NAME} INSTANCE_NAME=@INSTANCE_NAME@
MAIN_GUILD_NAME=${MAIN_GUILD_NAME} MAIN_GUILD_NAME=@MAIN_GUILD_NAME@
PROFILE=${PROFILE_RESOLVED} PROFILE=@PROFILE@
PROXY_MODE=${PROXY_MODE} PROXY_MODE=@PROXY_MODE@
APP_VERSION=${IMAGE_TAG} APP_VERSION=@IMAGE_TAG@
IMAGE_TAG=${IMAGE_TAG} IMAGE_TAG=@IMAGE_TAG@
APP_IMAGE=${APP_IMAGE} APP_IMAGE=@APP_IMAGE@
CADDY_IMAGE_TAG=${CADDY_IMAGE_TAG} CADDY_IMAGE_TAG=@CADDY_IMAGE_TAG@
LIVEKIT_IMAGE_TAG=${LIVEKIT_IMAGE_TAG} LIVEKIT_IMAGE_TAG=@LIVEKIT_IMAGE_TAG@
LOG_LEVEL=@LOG_LEVEL@
# --- TLS --- # --- TLS ---
# TLS считается включённым и когда сертификаты обслуживает внешний прокси TLS_ENABLED=@TLS_ENABLED@
# (--external-proxy --tls-terminated-by-proxy): клиентские ссылки должны быть https/wss. SKIP_TLS=@SKIP_TLS@
if [ "$SKIP_TLS" = "1" ] && [ "$TLS_BY_PROXY" != "true" ]; then TLS_BY_PROXY=@TLS_BY_PROXY@
TLS_ENABLED=false
else
TLS_ENABLED=true
fi
SKIP_TLS=${SKIP_TLS}
TLS_BY_PROXY=${TLS_BY_PROXY}
# --- интерполяция compose (пути и профиль) --- # --- интерполяция compose (пути, адреса привязки, профиль) ---
GLCHAT_DATA_DIR=${GLCHAT_DATA_DIR} GLCHAT_DATA_DIR=@GLCHAT_DATA_DIR@
GLCHAT_CADDY_PROFILE=${CADDY_PROFILE} GLCHAT_CADDY_PROFILE=@CADDY_PROFILE@
GLCHAT_TURN_CERT_DIR=${GLCHAT_TURN_CERT_DIR} GLCHAT_TURN_CERT_DIR=@GLCHAT_TURN_CERT_DIR@
GLCHAT_APP_BIND=${GLCHAT_APP_BIND} GLCHAT_APP_BIND=@GLCHAT_APP_BIND@
GLCHAT_LIVEKIT_BIND=${GLCHAT_LIVEKIT_BIND} GLCHAT_LIVEKIT_BIND=@GLCHAT_LIVEKIT_BIND@
TURN_RELAY_HOST=${TURN_RELAY_HOST_RESOLVED}
TURN_RELAY_PORT=${TURN_RELAY_PORT}
TURN_RELAY_TLS_PORT=${TURN_RELAY_TLS_PORT}
TURN_SECRET=${TURN_SECRET_RESOLVED}
# --- порты --- # --- порты ---
APP_PORT=${APP_PORT} APP_PORT=@APP_PORT@
LIVEKIT_WS_PORT=${LIVEKIT_WS_PORT} LIVEKIT_WS_PORT=@LIVEKIT_WS_PORT@
LIVEKIT_TCP_PORT=${LIVEKIT_TCP_PORT} LIVEKIT_TCP_PORT=@LIVEKIT_TCP_PORT@
LIVEKIT_UDP_PORT=${LIVEKIT_UDP_PORT} LIVEKIT_UDP_PORT=@LIVEKIT_UDP_PORT@
TURN_UDP_PORT=${TURN_UDP_PORT} HTTP_PORT=@HTTP_PORT@
TURN_TLS_PORT=${TURN_TLS_PORT} HTTPS_PORT=@HTTPS_PORT@
HTTP_PORT=${HTTP_PORT} SSH_PORT=@SSH_PORT@
HTTPS_PORT=${HTTPS_PORT}
SSH_PORT=${SSH_PORT} # --- TURN ---
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@
TURN_RELAY_START=@TURN_RELAY_START@
TURN_RELAY_END=@TURN_RELAY_END@
TURN_RELAY_HOST=@TURN_RELAY_HOST@
TURN_RELAY_PORT=@TURN_RELAY_PORT@
TURN_RELAY_TLS_PORT=@TURN_RELAY_TLS_PORT@
# --- секреты (генерируются один раз) --- # --- секреты (генерируются один раз) ---
SESSION_PEPPER=${SESSION_PEPPER} SESSION_PEPPER=@SESSION_PEPPER@
MASTER_KEY=${MASTER_KEY} MASTER_KEY=@MASTER_KEY@
TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY} TOTP_ENCRYPTION_KEY=@TOTP_ENCRYPTION_KEY@
WEBHOOK_SECRET=${WEBHOOK_SECRET} WEBHOOK_SECRET=@WEBHOOK_SECRET@
LIVEKIT_API_KEY=${LIVEKIT_API_KEY} LIVEKIT_API_KEY=@LIVEKIT_API_KEY@
LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET} LIVEKIT_API_SECRET=@LIVEKIT_API_SECRET@
AGE_RECIPIENT=${AGE_RECIPIENT:-} TURN_SECRET=@TURN_SECRET@
AGE_RECIPIENT=@AGE_RECIPIENT@
# --- профиль производительности --- # --- профиль производительности ---
SQLITE_READ_POOL=${PROFILE_SQLITE_READ_POOL} SQLITE_READ_POOL=@SQLITE_READ_POOL@
SQLITE_CACHE_SIZE=-${PROFILE_SQLITE_CACHE_MB}000 SQLITE_CACHE_SIZE=@SQLITE_CACHE_SIZE@
SQLITE_MMAP_SIZE=$((PROFILE_SQLITE_MMAP_MB * 1024 * 1024)) SQLITE_MMAP_SIZE=@SQLITE_MMAP_SIZE@
GOMAXPROCS=${PROFILE_GOMAXPROCS} GOMAXPROCS=@GOMAXPROCS@
LRU_CACHE_ENTRIES=${PROFILE_LRU_ENTRIES} LRU_CACHE_ENTRIES=@LRU_CACHE_ENTRIES@
MAX_UPLOAD_SIZE=$((EFFECTIVE_UPLOAD_MB * 1024 * 1024)) MAX_UPLOAD_SIZE=@MAX_UPLOAD_SIZE@
PROFILE_UPLOAD_MB=${EFFECTIVE_UPLOAD_MB} PROFILE_UPLOAD_MB=@PROFILE_UPLOAD_MB@
APP_CPUS=${PROFILE_APP_CPUS} APP_CPUS=@APP_CPUS@
APP_MEMORY_LIMIT=${PROFILE_APP_LIMIT} APP_MEMORY_LIMIT=@APP_MEMORY_LIMIT@
LIVEKIT_CPUS=${PROFILE_LIVEKIT_CPUS} LIVEKIT_CPUS=@LIVEKIT_CPUS@
LIVEKIT_MEMORY_LIMIT=${PROFILE_LIVEKIT_LIMIT} LIVEKIT_MEMORY_LIMIT=@LIVEKIT_MEMORY_LIMIT@
CADDY_CPUS=1.0 CADDY_CPUS=1.0
CADDY_MEMORY_LIMIT=256m CADDY_MEMORY_LIMIT=256m
LIVEKIT_MAX_PARTICIPANTS=${PROFILE_LIVEKIT_MAX_PARTICIPANTS} LIVEKIT_MAX_PARTICIPANTS=@LIVEKIT_MAX_PARTICIPANTS@
LIVEKIT_MAX_VIDEO=${PROFILE_LIVEKIT_MAX_VIDEO} LIVEKIT_MAX_VIDEO=@LIVEKIT_MAX_VIDEO@
LIVEKIT_DISABLE_SIMULCAST=${PROFILE_LIVEKIT_DISABLE_SIMULCAST} LIVEKIT_DISABLE_SIMULCAST=@LIVEKIT_DISABLE_SIMULCAST@
LIVEKIT_USE_EXTERNAL_IP=${PROFILE_LIVEKIT_USE_EXTERNAL_IP} LIVEKIT_USE_EXTERNAL_IP=@LIVEKIT_USE_EXTERNAL_IP@
SHARE_SOFT_LIMIT=${PROFILE_SHARE_SOFT} SHARE_SOFT_LIMIT=@SHARE_SOFT@
SHARE_MAX_LIMIT=${PROFILE_SHARE_MAX} SHARE_MAX_LIMIT=@SHARE_MAX@
BACKUP_KEEP_DAYS=${PROFILE_BACKUP_KEEP} BACKUP_KEEP_DAYS=@BACKUP_KEEP@
SWAP_SIZE_GB=${PROFILE_SWAP_GB} SWAP_SIZE_GB=@SWAP_SIZE@
# --- функции --- # --- функции ---
REGISTRATION_ENABLED=${REGISTRATION_ENABLED} REGISTRATION_ENABLED=@REGISTRATION_ENABLED@
ANTI_BOT_ENABLED=${ANTI_BOT_ENABLED:-false} ANTI_BOT_ENABLED=@ANTI_BOT_ENABLED@
TURN_ENABLED=${TURN_ENABLED} FAIL2BAN_ENABLED=@FAIL2BAN_ENABLED@
TURN_EXTERNAL_TLS=${TURN_EXTERNAL_TLS} UFW_ENABLED=@UFW_ENABLED@
FAIL2BAN_ENABLED=${ENABLE_FAIL2BAN} NFTABLES_ENABLED=@NFTABLES_ENABLED@
UFW_ENABLED=${ENABLE_UFW} SSH_HARDENING=@SSH_HARDENING@
NFTABLES_ENABLED=${ENABLE_NFTABLES}
SSH_HARDENING=${SSH_HARDENING}
LOG_LEVEL=${LOG_LEVEL}
# --- первичный администратор (используется один раз при bootstrap) --- # --- первичный администратор (используется один раз при bootstrap) ---
ADMIN_EMAIL=${ADMIN_EMAIL} ADMIN_EMAIL=@ADMIN_EMAIL@
ADMIN_PASSWORD=${ADMIN_PASSWORD} ADMIN_PASSWORD=@ADMIN_PASSWORD@
EOF ENVEOF
local pairs=(
"DOMAIN=${DOMAIN}"
"FILES_DOMAIN=${FILES_DOMAIN_RESOLVED}"
"INSTANCE_NAME=${INSTANCE_NAME}"
"MAIN_GUILD_NAME=${MAIN_GUILD_NAME}"
"PROFILE=${PROFILE_RESOLVED}"
"PROXY_MODE=${PROXY_MODE}"
"IMAGE_TAG=${IMAGE_TAG}"
"APP_IMAGE=${APP_IMAGE}"
"CADDY_IMAGE_TAG=${CADDY_IMAGE_TAG}"
"LIVEKIT_IMAGE_TAG=${LIVEKIT_IMAGE_TAG}"
"LOG_LEVEL=${LOG_LEVEL}"
"TLS_ENABLED=${TLS_ENABLED}"
"SKIP_TLS=${SKIP_TLS}"
"TLS_BY_PROXY=${TLS_BY_PROXY}"
"GLCHAT_DATA_DIR=${GLCHAT_DATA_DIR}"
"CADDY_PROFILE=${CADDY_PROFILE}"
"GLCHAT_TURN_CERT_DIR=${TURN_CERT_DIR}"
"GLCHAT_APP_BIND=${APP_BIND_ADDR}"
"GLCHAT_LIVEKIT_BIND=${LIVEKIT_BIND_ADDR}"
"APP_PORT=${APP_PORT}"
"LIVEKIT_WS_PORT=${LIVEKIT_WS_PORT}"
"LIVEKIT_TCP_PORT=${LIVEKIT_TCP_PORT}"
"LIVEKIT_UDP_PORT=${LIVEKIT_UDP_PORT}"
"HTTP_PORT=${HTTP_PORT}"
"HTTPS_PORT=${HTTPS_PORT}"
"SSH_PORT=${SSH_PORT}"
"TURN_ENABLED=${TURN_ENABLED}"
"TURN_DOMAIN=${TURN_DOMAIN_RESOLVED}"
"TURN_UDP_PORT=${TURN_UDP_PORT}"
"TURN_TLS_PORT=${TURN_TLS_PORT}"
"TURN_EXTERNAL_TLS=${TURN_EXTERNAL_TLS}"
"TURN_RELAY_START=${TURN_RELAY_START}"
"TURN_RELAY_END=${TURN_RELAY_END}"
"TURN_RELAY_HOST=${TURN_RELAY_HOST_RESOLVED}"
"TURN_RELAY_PORT=${TURN_RELAY_PORT}"
"TURN_RELAY_TLS_PORT=${TURN_RELAY_TLS_PORT}"
"SESSION_PEPPER=${SESSION_PEPPER}"
"MASTER_KEY=${MASTER_KEY}"
"TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY}"
"WEBHOOK_SECRET=${WEBHOOK_SECRET}"
"LIVEKIT_API_KEY=${LIVEKIT_API_KEY}"
"LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET}"
"TURN_SECRET=${TURN_SECRET_RESOLVED}"
"AGE_RECIPIENT=${AGE_RECIPIENT:-}"
"SQLITE_READ_POOL=${PROFILE_SQLITE_READ_POOL}"
"SQLITE_CACHE_SIZE=-${PROFILE_SQLITE_CACHE_MB}000"
"SQLITE_MMAP_SIZE=$((PROFILE_SQLITE_MMAP_MB * 1024 * 1024))"
"GOMAXPROCS=${PROFILE_GOMAXPROCS}"
"LRU_CACHE_ENTRIES=${PROFILE_LRU_ENTRIES}"
"MAX_UPLOAD_SIZE=$((EFFECTIVE_UPLOAD_MB * 1024 * 1024))"
"PROFILE_UPLOAD_MB=${EFFECTIVE_UPLOAD_MB}"
"APP_CPUS=${PROFILE_APP_CPUS}"
"APP_MEMORY_LIMIT=${PROFILE_APP_LIMIT}"
"LIVEKIT_CPUS=${PROFILE_LIVEKIT_CPUS}"
"LIVEKIT_MEMORY_LIMIT=${PROFILE_LIVEKIT_LIMIT}"
"LIVEKIT_MAX_PARTICIPANTS=${PROFILE_LIVEKIT_MAX_PARTICIPANTS}"
"LIVEKIT_MAX_VIDEO=${PROFILE_LIVEKIT_MAX_VIDEO}"
"LIVEKIT_DISABLE_SIMULCAST=${PROFILE_LIVEKIT_DISABLE_SIMULCAST}"
"LIVEKIT_USE_EXTERNAL_IP=${PROFILE_LIVEKIT_USE_EXTERNAL_IP}"
"SHARE_SOFT=${PROFILE_SHARE_SOFT}"
"SHARE_MAX=${PROFILE_SHARE_MAX}"
"BACKUP_KEEP=${PROFILE_BACKUP_KEEP}"
"SWAP_SIZE=${PROFILE_SWAP_GB}"
"REGISTRATION_ENABLED=${REGISTRATION_ENABLED}"
"ANTI_BOT_ENABLED=${ANTI_BOT_ENABLED:-false}"
"FAIL2BAN_ENABLED=${ENABLE_FAIL2BAN}"
"UFW_ENABLED=${ENABLE_UFW}"
"NFTABLES_ENABLED=${ENABLE_NFTABLES}"
"SSH_HARDENING=${SSH_HARDENING}"
"ADMIN_EMAIL=${ADMIN_EMAIL}"
"ADMIN_PASSWORD=${ADMIN_PASSWORD}"
)
local sed_args=()
local pair key value
for pair in "${pairs[@]}"; do
key="${pair%%=*}"
value="${pair#*=}"
sed_args+=(-e "s@@${key}@@${value}@g")
done
sed -i.bak "${sed_args[@]}" "$tmp"
install -m 600 "$tmp" "$GLCHAT_ENV_FILE" install -m 600 "$tmp" "$GLCHAT_ENV_FILE"
rm -f "$tmp" rm -f "$tmp" "$tmp.bak"
ok ".env записан (права 600)" ok ".env записан (права 600)"
} }