Развёртывание на Debian 12/13: Docker, nginx, работа за обратным прокси
- приложение перенесено в public/, инструкции в docs/ - Dockerfile (nginx без root-прав), docker-compose.yml с ограничениями - deploy/split-trainer.conf: CSP и заголовки безопасности, no-cache по ETag, /healthz, реальный IP из X-Forwarded-For только от частных сетей, редиректы без внутреннего хоста - deploy/install-debian.sh: установка и обновление без Docker - docs/DEPLOY.md: требования, установка, примеры прокси (nginx, Caddy, Traefik), подпуть Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
42b90a056c
commit
e897a6c5eb
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# Установка и обновление Сплит-тренера на Debian 12/13: nginx + статические файлы.
|
||||
#
|
||||
# sudo ./deploy/install-debian.sh # за обратным прокси: слушаем 127.0.0.1:8080
|
||||
# sudo LISTEN=80 DOMAIN=trainer.example.com ./deploy/install-debian.sh # без прокси, напрямую на порту 80
|
||||
#
|
||||
# Повторный запуск обновляет файлы сайта и конфиг. Ничего, кроме этого сайта, скрипт не трогает.
|
||||
set -euo pipefail
|
||||
|
||||
LISTEN="${LISTEN:-127.0.0.1:8080}" # адрес:порт или просто порт
|
||||
DOMAIN="${DOMAIN:-_}" # server_name; "_" = любое имя
|
||||
WEBROOT="${WEBROOT:-/var/www/split-trainer}"
|
||||
NAME="split-trainer"
|
||||
|
||||
die() { echo "Ошибка: $*" >&2; exit 1; }
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "запустите от root: sudo $0"
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SRC="$HERE/../public"
|
||||
CONF_TEMPLATE="$HERE/$NAME.conf"
|
||||
[ -f "$SRC/index.html" ] || die "не найден $SRC/index.html: запускайте скрипт из клонированного репозитория"
|
||||
[ -f "$CONF_TEMPLATE" ] || die "не найден $CONF_TEMPLATE"
|
||||
|
||||
# Значения подставляются в sed: не пускаем туда ничего, кроме безопасных символов
|
||||
[[ "$LISTEN" =~ ^[][0-9A-Za-z.:_-]+$ ]] || die "некорректный LISTEN: $LISTEN"
|
||||
[[ "$DOMAIN" =~ ^[0-9A-Za-z._*-]+( [0-9A-Za-z._*-]+)*$ ]] || die "некорректный DOMAIN: $DOMAIN"
|
||||
[[ "$WEBROOT" =~ ^/[0-9A-Za-z._/-]+$ ]] || die "некорректный WEBROOT: $WEBROOT"
|
||||
|
||||
. /etc/os-release
|
||||
if [ "${ID:-}" != "debian" ] || { [ "${VERSION_ID:-}" != "12" ] && [ "${VERSION_ID:-}" != "13" ]; }; then
|
||||
[ "${FORCE:-0}" = "1" ] || die "скрипт рассчитан на Debian 12/13 (у вас: ${PRETTY_NAME:-неизвестно}). Чтобы всё равно продолжить: FORCE=1"
|
||||
fi
|
||||
|
||||
echo "==> Устанавливаю nginx"
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq --no-install-recommends nginx >/dev/null
|
||||
|
||||
echo "==> Копирую сайт в $WEBROOT"
|
||||
install -d -m 755 "$WEBROOT"
|
||||
find "$WEBROOT" -mindepth 1 -maxdepth 1 -type f -delete # убираем только файлы прошлой версии
|
||||
install -m 644 "$SRC"/* "$WEBROOT"/
|
||||
|
||||
echo "==> Настраиваю nginx (listen $LISTEN, server_name $DOMAIN)"
|
||||
CONF="/etc/nginx/sites-available/$NAME.conf"
|
||||
sed -e "s|^\(\s*\)listen 8080;.*|\1listen $LISTEN;|" \
|
||||
-e "s|^\(\s*\)server_name _;.*|\1server_name $DOMAIN;|" \
|
||||
-e "s|^\(\s*\)root /usr/share/nginx/html;.*|\1root $WEBROOT;|" \
|
||||
"$CONF_TEMPLATE" > "$CONF"
|
||||
ln -sf "$CONF" "/etc/nginx/sites-enabled/$NAME.conf"
|
||||
|
||||
nginx -t
|
||||
systemctl enable --now nginx >/dev/null 2>&1
|
||||
systemctl reload nginx
|
||||
|
||||
# Проверяем, что сайт действительно отвечает
|
||||
HOSTPORT="$LISTEN"; [[ "$HOSTPORT" =~ ^[0-9]+$ ]] && HOSTPORT="127.0.0.1:$HOSTPORT"
|
||||
[ "${HOSTPORT%%:*}" = "0.0.0.0" ] && HOSTPORT="127.0.0.1:${HOSTPORT##*:}"
|
||||
if command -v curl >/dev/null 2>&1 && ! curl -fsS -m 5 -o /dev/null "http://$HOSTPORT/healthz" -H "Host: ${DOMAIN%% *}"; then
|
||||
echo "Предупреждение: проверка http://$HOSTPORT/healthz не прошла. Если порт 80 занят сайтом по умолчанию, см. docs/DEPLOY.md" >&2
|
||||
fi
|
||||
|
||||
echo "==> Готово: http://$HOSTPORT/"
|
||||
@@ -0,0 +1,58 @@
|
||||
# Сплит-тренер: статический сайт. Один и тот же файл годится и для контейнера, и для nginx на хосте.
|
||||
# Скрипт deploy/install-debian.sh заменяет ровно три строки с пометкой "# <-": listen, server_name и root.
|
||||
# Совместимо с nginx 1.22 (Debian 12) и 1.26 (Debian 13).
|
||||
|
||||
server {
|
||||
listen 8080; # <- порт или адрес:порт
|
||||
server_name _; # <- имя сайта
|
||||
root /usr/share/nginx/html; # <- каталог с файлами
|
||||
index index.html;
|
||||
|
||||
server_tokens off;
|
||||
|
||||
# За обратным прокси редиректы не должны подставлять внутренний хост и порт
|
||||
absolute_redirect off;
|
||||
port_in_redirect off;
|
||||
|
||||
# Настоящий адрес клиента (для логов) берём из X-Forwarded-For, но только от прокси из частных сетей и localhost
|
||||
set_real_ip_from 127.0.0.1;
|
||||
set_real_ip_from ::1;
|
||||
set_real_ip_from 10.0.0.0/8;
|
||||
set_real_ip_from 172.16.0.0/12;
|
||||
set_real_ip_from 192.168.0.0/16;
|
||||
set_real_ip_from fc00::/7;
|
||||
real_ip_header X-Forwarded-For;
|
||||
real_ip_recursive on;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 512;
|
||||
gzip_types text/css application/javascript text/javascript text/plain application/json image/svg+xml;
|
||||
|
||||
# Проверка живости для мониторинга и healthcheck контейнера
|
||||
location = /healthz {
|
||||
access_log off;
|
||||
default_type text/plain;
|
||||
return 200 "ok\n";
|
||||
}
|
||||
|
||||
# Все заголовки безопасности заданы в одном location: add_header не наследуется между уровнями.
|
||||
# no-cache не запрещает кеш, а заставляет браузер сверять версию (ETag) при каждом открытии:
|
||||
# после обновления никто не застрянет на старом app.js.
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
|
||||
add_header Cache-Control "no-cache" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
|
||||
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'" always;
|
||||
# HSTS не ставим здесь: HTTPS завершает внешний прокси, он и должен отдавать Strict-Transport-Security.
|
||||
}
|
||||
|
||||
# Скрытые файлы (.git, .env и т. п.) не отдаём никогда
|
||||
location ~ /\. {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user