chore(deploy): Docker-образы, compose, deploy.sh, setup-nginx.sh
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
# зависимости и артефакты сборки — пересобираются внутри образа
|
||||
**/node_modules
|
||||
**/.next
|
||||
**/dist
|
||||
**/out
|
||||
**/coverage
|
||||
**/*.tsbuildinfo
|
||||
**/next-env.d.ts
|
||||
.pnpm-store
|
||||
|
||||
# vcs, служебное агентов и редакторов
|
||||
.git
|
||||
.claude
|
||||
.worktrees
|
||||
.idea
|
||||
.vscode
|
||||
**/.DS_Store
|
||||
|
||||
# секреты и данные
|
||||
**/.env
|
||||
**/.env.*
|
||||
**/*.env
|
||||
data
|
||||
**/data
|
||||
**/*.sqlite
|
||||
**/*.sqlite-*
|
||||
**/*.sqlite.gz
|
||||
|
||||
# артефакты проверок
|
||||
.screenshots
|
||||
.lighthouse
|
||||
test-results
|
||||
playwright-report
|
||||
|
||||
# не нужно для сборки образов
|
||||
docs
|
||||
deploy
|
||||
**/*.md
|
||||
@@ -0,0 +1,69 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Сборка из корня репо:
|
||||
# docker buildx build --platform linux/amd64 -f deploy/Dockerfile.api \
|
||||
# --build-arg APP_VERSION=$(git rev-parse --short HEAD) -t kvarta-api .
|
||||
|
||||
# builder на том же node:22-slim (glibc), что и рантайм: нативный better-sqlite3,
|
||||
# скачанный/собранный здесь под целевую платформу, гарантированно совместим
|
||||
FROM node:22-slim AS build
|
||||
# toolchain нужен только если prebuilt better-sqlite3 не скачается; в рантайм не попадает
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends python3 make g++ ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# corepack из node 22 может не знать ключи свежего pnpm — обновляем его
|
||||
RUN npm install -g corepack@latest >/dev/null && corepack enable
|
||||
WORKDIR /repo
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json ./
|
||||
COPY apps/api/package.json apps/api/
|
||||
COPY packages/shared/package.json packages/shared/
|
||||
RUN corepack install
|
||||
RUN --mount=type=cache,id=kvarta-pnpm-slim,target=/pnpm-store \
|
||||
pnpm install --frozen-lockfile --store-dir /pnpm-store \
|
||||
--filter kvarta --filter "@kvarta/api..."
|
||||
COPY apps/api apps/api
|
||||
COPY packages/shared packages/shared
|
||||
RUN pnpm --filter @kvarta/api build
|
||||
# отдельный каталог только с production-зависимостями api (shared уже вшит tsup'ом)
|
||||
RUN --mount=type=cache,id=kvarta-pnpm-slim,target=/pnpm-store \
|
||||
pnpm --filter @kvarta/api deploy --prod --legacy --store-dir /pnpm-store /out \
|
||||
&& rm -rf /out/src /out/tsconfig.json /out/tsup.config.ts /out/vitest.config.ts
|
||||
# better-sqlite3 13 везёт prebuilt-бинарники всех платформ (~17 МБ) и исходники SQLite (~10 МБ):
|
||||
# оставляем только glibc-бинарник своей архитектуры (linux-x64 для linux/amd64)
|
||||
RUN set -eu; \
|
||||
bs=$(readlink -f /out/node_modules/better-sqlite3); \
|
||||
keep="linux-$(node -p process.arch).node"; \
|
||||
test -f "$bs/prebuilds/$keep"; \
|
||||
find "$bs/prebuilds" -type f ! -name "$keep" -delete; \
|
||||
rm -rf "$bs/deps" "$bs/src" "$bs/build" "$bs/binding.gyp" \
|
||||
/out/node_modules/.pnpm/node-addon-api@*; \
|
||||
find /out/node_modules -name '*.md' -type f -delete; \
|
||||
cd /out && node -e "const D=require('better-sqlite3'); const db=new D(':memory:'); console.log('better-sqlite3 ok, sqlite', db.prepare('select sqlite_version() v').get().v); db.close()"
|
||||
|
||||
# рантайму не нужны npm/corepack/yarn и документация (~25 МБ); удалить их слоем поверх базы
|
||||
# нельзя — размер не уменьшится, поэтому чистим и сплющиваем базу в один слой через scratch
|
||||
FROM node:22-slim AS node-base
|
||||
RUN rm -rf /usr/local/lib/node_modules /usr/local/bin/npm /usr/local/bin/npx \
|
||||
/usr/local/bin/corepack /usr/local/bin/yarn /usr/local/bin/yarnpkg /opt/yarn-* \
|
||||
/usr/local/include /usr/local/share/doc /usr/local/share/man \
|
||||
/usr/share/doc /usr/share/man /usr/share/info /usr/share/locale \
|
||||
/var/lib/apt/lists/* /var/cache/* /var/log/* /root/.npm /tmp/* \
|
||||
&& groupadd -r -g 1001 kvarta \
|
||||
&& useradd -r -u 1001 -g kvarta -d /app -s /usr/sbin/nologin kvarta \
|
||||
&& mkdir -p /app /data && chown kvarta:kvarta /data
|
||||
|
||||
FROM scratch AS runtime
|
||||
COPY --from=node-base / /
|
||||
ARG APP_VERSION=dev
|
||||
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||
NODE_ENV=production \
|
||||
PORT=3000 \
|
||||
DB_PATH=/data/kvarta.sqlite \
|
||||
APP_VERSION=${APP_VERSION}
|
||||
WORKDIR /app
|
||||
COPY --from=build --chown=kvarta:kvarta /out ./
|
||||
USER kvarta
|
||||
VOLUME ["/data"]
|
||||
EXPOSE 3000
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD ["node", "-e", "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/health').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
|
||||
CMD ["node", "dist/index.js"]
|
||||
@@ -0,0 +1,45 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Сборка из корня репо:
|
||||
# docker buildx build --platform linux/amd64 -f deploy/Dockerfile.landing -t kvarta-landing .
|
||||
|
||||
FROM node:22-alpine AS build
|
||||
# corepack из node 22 может не знать ключи свежего pnpm — обновляем его
|
||||
RUN npm install -g corepack@latest >/dev/null && corepack enable
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
WORKDIR /repo
|
||||
# сначала только манифесты — слой с зависимостями кешируется
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json ./
|
||||
COPY apps/landing/package.json apps/landing/
|
||||
COPY packages/shared/package.json packages/shared/
|
||||
RUN corepack install
|
||||
RUN --mount=type=cache,id=kvarta-pnpm-alpine,target=/pnpm-store \
|
||||
pnpm install --frozen-lockfile --store-dir /pnpm-store \
|
||||
--filter kvarta --filter "@kvarta/landing..."
|
||||
COPY apps/landing apps/landing
|
||||
COPY packages/shared packages/shared
|
||||
RUN pnpm --filter @kvarta/landing build \
|
||||
&& mkdir -p apps/landing/public
|
||||
|
||||
# рантайму не нужны npm/corepack/yarn (~23 МБ); удалить их слоем поверх базы нельзя —
|
||||
# размер не уменьшится, поэтому чистим и сплющиваем базу в один слой через scratch
|
||||
FROM node:22-alpine AS node-base
|
||||
RUN rm -rf /usr/local/lib/node_modules /usr/local/bin/npm /usr/local/bin/npx \
|
||||
/usr/local/bin/corepack /usr/local/bin/yarn /usr/local/bin/yarnpkg /opt/yarn-* \
|
||||
/usr/local/include /root/.npm /tmp/* \
|
||||
&& addgroup -S -g 1001 kvarta && adduser -S -u 1001 -G kvarta kvarta
|
||||
|
||||
FROM scratch AS runtime
|
||||
COPY --from=node-base / /
|
||||
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||
NODE_ENV=production \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
PORT=3000 \
|
||||
HOSTNAME=0.0.0.0
|
||||
WORKDIR /app
|
||||
# outputFileTracingRoot = корень репо, поэтому server.js лежит в apps/landing/ внутри standalone
|
||||
COPY --from=build --chown=kvarta:kvarta /repo/apps/landing/.next/standalone ./
|
||||
COPY --from=build --chown=kvarta:kvarta /repo/apps/landing/.next/static ./apps/landing/.next/static
|
||||
COPY --from=build --chown=kvarta:kvarta /repo/apps/landing/public ./apps/landing/public
|
||||
USER kvarta
|
||||
EXPOSE 3000
|
||||
CMD ["node", "apps/landing/server.js"]
|
||||
@@ -0,0 +1,14 @@
|
||||
# Секреты api. На сервере: /root/kvarta_site/api.env, chmod 600, заполняет автор.
|
||||
# deploy.sh кладёт этот шаблон, только если api.env на сервере ещё нет, и дальше его не трогает.
|
||||
# PORT, DB_PATH и APP_VERSION задаёт образ — здесь их не указывать.
|
||||
|
||||
# Токен бота от @BotFather
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
# ID группы с темами, куда бот пишет (отрицательное число вида -100…)
|
||||
TELEGRAM_CHAT_ID=
|
||||
# ID темы «Заявки» (message_thread_id) — сюда приходят новые заявки
|
||||
TELEGRAM_LEADS_THREAD_ID=
|
||||
# ID темы «Система» — ошибки, бэкапы, служебные сообщения
|
||||
TELEGRAM_SYSTEM_THREAD_ID=
|
||||
# Telegram user ID автора: только ему бот отвечает на команды администрирования
|
||||
TELEGRAM_ADMIN_ID=
|
||||
Executable
+230
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env bash
|
||||
# Деплой «Кварты» на VPS. Запускать локально из корня репозитория:
|
||||
# deploy/deploy.sh — собрать HEAD и выкатить
|
||||
# deploy/deploy.sh --rollback — вернуть предыдущий тег из /root/kvarta_site/.deployed
|
||||
# Хост: DEPLOY_HOST (по умолчанию root@185.5.249.52).
|
||||
#
|
||||
# На сервере рядом чужие боевые сайты: скрипт трогает только /root/kvarta_site,
|
||||
# контейнеры проекта kvarta и образы kvarta-landing/kvarta-api. Никаких prune.
|
||||
set -euo pipefail
|
||||
|
||||
DEPLOY_HOST="${DEPLOY_HOST:-root@185.5.249.52}"
|
||||
REMOTE_DIR=/root/kvarta_site
|
||||
COMPOSE_FILE=docker-compose_vps.yml
|
||||
PROJECT=kvarta
|
||||
REPOS=(kvarta-landing kvarta-api)
|
||||
MIN_FREE_MB=1024
|
||||
HEALTH_TIMEOUT=30
|
||||
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=10
|
||||
-o ControlMaster=auto -o "ControlPath=$HOME/.ssh/kvarta-deploy-%C" -o ControlPersist=60)
|
||||
|
||||
STEP=0
|
||||
step() { STEP=$((STEP + 1)); printf '\n\033[1m[%d] %s\033[0m\n' "$STEP" "$*"; }
|
||||
info() { printf ' %s\n' "$*"; }
|
||||
warn() { printf '\033[33m ВНИМАНИЕ: %s\033[0m\n' "$*" >&2; }
|
||||
die() { printf '\033[31mОШИБКА: %s\033[0m\n' "$*" >&2; exit 1; }
|
||||
# shellcheck disable=SC2029 # команды для сервера собираются локально намеренно
|
||||
remote() { ssh "${SSH_OPTS[@]}" "$DEPLOY_HOST" "$@"; }
|
||||
|
||||
usage() {
|
||||
sed -n '2,5p' "$0" | sed 's/^# \{0,1\}//'
|
||||
}
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# --- общие шаги на сервере ---------------------------------------------------
|
||||
|
||||
check_server() {
|
||||
step "Проверка сервера $DEPLOY_HOST"
|
||||
remote true || die "нет ssh-доступа к $DEPLOY_HOST (нужен ключ, BatchMode)"
|
||||
remote 'command -v docker-compose >/dev/null' \
|
||||
|| die "на сервере нет docker-compose (ожидается v2 в /usr/local/bin)"
|
||||
info "$(remote 'docker-compose version --short 2>/dev/null || docker-compose version' | head -1)"
|
||||
}
|
||||
|
||||
# compose up с указанным тегом, затем ждём health обоих сервисов
|
||||
compose_up() {
|
||||
local tag=$1
|
||||
step "Запуск контейнеров с тегом $tag"
|
||||
remote "cd $REMOTE_DIR && KVARTA_TAG=$tag docker-compose -f $COMPOSE_FILE -p $PROJECT up -d --remove-orphans"
|
||||
|
||||
step "Ожидание здоровья (до ${HEALTH_TIMEOUT} с)"
|
||||
if remote bash -s -- "$HEALTH_TIMEOUT" <<'REMOTE'
|
||||
set -u
|
||||
for _ in $(seq 1 "$1"); do
|
||||
if curl -fs -o /dev/null http://127.0.0.1:13121/api/health \
|
||||
&& curl -fs -o /dev/null http://127.0.0.1:13120/; then
|
||||
curl -fsS http://127.0.0.1:13121/api/health; echo
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
exit 1
|
||||
REMOTE
|
||||
then
|
||||
info "api и landing отвечают"
|
||||
else
|
||||
warn "сервисы не ответили за ${HEALTH_TIMEOUT} с; последние логи:"
|
||||
remote "cd $REMOTE_DIR && docker-compose -f $COMPOSE_FILE -p $PROJECT logs --tail 40" || true
|
||||
die "деплой $tag не поднялся. Откат: deploy/deploy.sh --rollback"
|
||||
fi
|
||||
}
|
||||
|
||||
# latest на сервере — всегда то, что сейчас запущено (для ручного docker-compose up)
|
||||
retag_latest() {
|
||||
local tag=$1 repo
|
||||
for repo in "${REPOS[@]}"; do
|
||||
remote "docker tag $repo:$tag $repo:latest"
|
||||
done
|
||||
}
|
||||
|
||||
# .deployed: current=<tag> / previous=<tag>
|
||||
read_deployed() {
|
||||
remote "cat $REMOTE_DIR/.deployed 2>/dev/null || true"
|
||||
}
|
||||
|
||||
write_deployed() {
|
||||
local current=$1 previous=$2
|
||||
remote "printf 'current=%s\nprevious=%s\n' '$current' '$previous' > $REMOTE_DIR/.deployed"
|
||||
info "current=$current previous=${previous:-—}"
|
||||
}
|
||||
|
||||
# --- откат -------------------------------------------------------------------
|
||||
|
||||
rollback() {
|
||||
check_server
|
||||
step "Чтение $REMOTE_DIR/.deployed"
|
||||
local state current previous repo
|
||||
state=$(read_deployed)
|
||||
current=$(sed -n 's/^current=//p' <<<"$state")
|
||||
previous=$(sed -n 's/^previous=//p' <<<"$state")
|
||||
[[ -n $previous ]] || die "в .deployed нет предыдущего тега — откатываться некуда"
|
||||
info "сейчас $current, откатываемся на $previous"
|
||||
for repo in "${REPOS[@]}"; do
|
||||
remote "docker image inspect $repo:$previous >/dev/null 2>&1" \
|
||||
|| die "на сервере нет образа $repo:$previous"
|
||||
done
|
||||
compose_up "$previous"
|
||||
retag_latest "$previous"
|
||||
step "Запись .deployed"
|
||||
# меняем местами: повторный --rollback вернёт обратно
|
||||
write_deployed "$previous" "$current"
|
||||
printf '\nГотово: откат на %s.\n' "$previous"
|
||||
}
|
||||
|
||||
# --- деплой ------------------------------------------------------------------
|
||||
|
||||
deploy() {
|
||||
step "Проверка рабочего дерева"
|
||||
if [[ -n $(git status --porcelain) ]]; then
|
||||
git status --short
|
||||
die "есть незакоммиченные или неотслеживаемые файлы — закоммитьте или уберите их"
|
||||
fi
|
||||
local sha
|
||||
sha=$(git rev-parse --short HEAD)
|
||||
info "коммит $sha: $(git log -1 --format=%s)"
|
||||
|
||||
check_server
|
||||
|
||||
step "Свободное место на сервере"
|
||||
local free_mb
|
||||
# меряем раздел, где docker хранит образы (туда ляжет docker load)
|
||||
free_mb=$(remote bash -s <<'REMOTE'
|
||||
dir=$(docker info -f '{{.DockerRootDir}}' 2>/dev/null)
|
||||
[ -d "$dir" ] || dir=/
|
||||
echo "$dir $(df -Pm "$dir" | awk 'NR==2 {print $4}')"
|
||||
REMOTE
|
||||
)
|
||||
local docker_root=${free_mb% *}
|
||||
free_mb=${free_mb##* }
|
||||
[[ $free_mb =~ ^[0-9]+$ ]] || die "не удалось узнать свободное место на сервере (получено: '$free_mb')"
|
||||
info "свободно ${free_mb} МБ на разделе с $docker_root"
|
||||
if ((free_mb < MIN_FREE_MB)); then
|
||||
die "на сервере меньше ${MIN_FREE_MB} МБ свободно (${free_mb} МБ). Освободите место (своё, не чужое) и повторите"
|
||||
fi
|
||||
|
||||
local need_upload=0 repo
|
||||
for repo in "${REPOS[@]}"; do
|
||||
remote "docker image inspect $repo:$sha >/dev/null 2>&1" || need_upload=1
|
||||
done
|
||||
|
||||
if ((need_upload)); then
|
||||
step "Сборка образов linux/amd64 ($sha)"
|
||||
local hint="если в логе 'exec format error' — на этой машине нет эмуляции amd64:"
|
||||
hint+=" Docker Desktop → Settings → General → «Use Rosetta for x86_64/amd64 emulation»"
|
||||
hint+=" или docker run --privileged --rm tonistiigi/binfmt --install amd64"
|
||||
local name
|
||||
for name in landing api; do
|
||||
info "kvarta-$name"
|
||||
docker buildx build --platform linux/amd64 --load \
|
||||
--provenance=false --sbom=false \
|
||||
-f "deploy/Dockerfile.$name" \
|
||||
--build-arg "APP_VERSION=$sha" \
|
||||
-t "kvarta-$name:$sha" -t "kvarta-$name:latest" \
|
||||
. || die "сборка kvarta-$name упала; $hint"
|
||||
done
|
||||
docker image ls --format ' {{.Repository}}:{{.Tag}} {{.Size}}' | grep -E "^ kvarta-(landing|api):$sha " || true
|
||||
|
||||
step "Передача образов на сервер (docker save | gzip | ssh docker load)"
|
||||
docker save "kvarta-landing:$sha" "kvarta-api:$sha" | gzip | remote 'gunzip | docker load'
|
||||
else
|
||||
step "Образы $sha уже есть на сервере — сборка и передача пропущены"
|
||||
fi
|
||||
|
||||
step "Файлы в $REMOTE_DIR"
|
||||
remote "mkdir -p $REMOTE_DIR"
|
||||
scp -q "${SSH_OPTS[@]}" "deploy/$COMPOSE_FILE" "$DEPLOY_HOST:$REMOTE_DIR/$COMPOSE_FILE"
|
||||
info "$COMPOSE_FILE обновлён"
|
||||
local env_state
|
||||
env_state=$(remote "cd $REMOTE_DIR && if [ -e api.env ]; then echo exists; else umask 077 && cat > api.env && chmod 600 api.env && echo created; fi" <deploy/api.env.example)
|
||||
if [[ $env_state == created ]]; then
|
||||
warn "api.env не было — положен пустой шаблон $REMOTE_DIR/api.env (chmod 600)."
|
||||
warn "Заполните его на сервере и перезапустите api: cd $REMOTE_DIR && docker-compose -f $COMPOSE_FILE -p $PROJECT up -d api"
|
||||
else
|
||||
info "api.env уже есть — не трогаем"
|
||||
fi
|
||||
|
||||
local state previous
|
||||
state=$(read_deployed)
|
||||
previous=$(sed -n 's/^current=//p' <<<"$state")
|
||||
# повторный деплой того же коммита не должен затирать предыдущий тег
|
||||
if [[ $previous == "$sha" ]]; then
|
||||
previous=$(sed -n 's/^previous=//p' <<<"$state")
|
||||
fi
|
||||
|
||||
compose_up "$sha"
|
||||
retag_latest "$sha"
|
||||
|
||||
step "Запись .deployed"
|
||||
write_deployed "$sha" "$previous"
|
||||
|
||||
step "Удаление старых образов kvarta-landing/kvarta-api (кроме $sha${previous:+ и $previous})"
|
||||
# ssh склеивает аргументы в строку — пустой previous пропал бы, поэтому заглушка "-"
|
||||
remote bash -s -- "$sha" "${previous:--}" "${REPOS[@]}" <<'REMOTE'
|
||||
set -u
|
||||
keep_current=$1 keep_previous=$2
|
||||
shift 2
|
||||
for repo in "$@"; do
|
||||
docker image ls "$repo" --format '{{.Tag}}' | while read -r tag; do
|
||||
case $tag in
|
||||
latest|"$keep_current"|"$keep_previous"|"<none>") continue ;;
|
||||
esac
|
||||
if docker rmi "$repo:$tag" >/dev/null; then
|
||||
echo " удалён $repo:$tag"
|
||||
else
|
||||
echo " не удалось удалить $repo:$tag (используется?) — пропускаю"
|
||||
fi
|
||||
done
|
||||
done
|
||||
REMOTE
|
||||
|
||||
printf '\nГотово: %s выкачен на %s.\n' "$sha" "$DEPLOY_HOST"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
"") deploy ;;
|
||||
--rollback) rollback ;;
|
||||
-h | --help) usage ;;
|
||||
*) usage; die "неизвестный аргумент: $1" ;;
|
||||
esac
|
||||
@@ -0,0 +1,37 @@
|
||||
# Прод на VPS: /root/kvarta_site/docker-compose_vps.yml (кладёт deploy/deploy.sh).
|
||||
# На сервере только docker-compose v2.23 (без плагина `docker compose`):
|
||||
# KVARTA_TAG=<sha> docker-compose -f docker-compose_vps.yml -p kvarta up -d
|
||||
# Наружу порты не торчат — только 127.0.0.1, снаружи ходит nginx хоста.
|
||||
name: kvarta
|
||||
|
||||
x-logging: &logging
|
||||
# диск сервера почти полный — логи ротируются: не больше 3 × 10 МБ на контейнер
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
services:
|
||||
landing:
|
||||
image: kvarta-landing:${KVARTA_TAG:-latest}
|
||||
container_name: kvarta-landing
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:13120:3000"
|
||||
logging: *logging
|
||||
|
||||
api:
|
||||
image: kvarta-api:${KVARTA_TAG:-latest}
|
||||
container_name: kvarta-api
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:13121:3000"
|
||||
env_file: api.env
|
||||
volumes:
|
||||
- kvarta-data:/data
|
||||
logging: *logging
|
||||
|
||||
volumes:
|
||||
kvarta-data:
|
||||
# явное имя: не зависит от project name, его ищут бэкапы и восстановление
|
||||
name: kvarta-data
|
||||
@@ -0,0 +1,42 @@
|
||||
# /etc/nginx/sites-available/kvarta.unuchak.com (ставит deploy/setup-nginx.sh)
|
||||
# Только 80-й порт: certbot --nginx сам допишет 443-блок и редирект.
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name kvarta.unuchak.com;
|
||||
|
||||
# заявка — это небольшой JSON; всё крупнее отсекаем ещё на nginx
|
||||
client_max_body_size 32k;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 5;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml application/javascript application/json
|
||||
application/xml application/rss+xml image/svg+xml application/manifest+json;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:13121;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:13120;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
}
|
||||
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
# Однократная настройка nginx и HTTPS для kvarta.unuchak.com. Запускать локально из корня:
|
||||
# CERTBOT_EMAIL=you@example.com deploy/setup-nginx.sh
|
||||
# Хост: DEPLOY_HOST (по умолчанию root@185.5.249.52). Повторный запуск безопасен.
|
||||
#
|
||||
# На сервере чужие боевые сайты: при ошибке `nginx -t` наш симлинк убирается
|
||||
# и reload не делается — общий nginx не должен упасть из-за нашего конфига.
|
||||
set -euo pipefail
|
||||
|
||||
DEPLOY_HOST="${DEPLOY_HOST:-root@185.5.249.52}"
|
||||
DOMAIN=kvarta.unuchak.com
|
||||
CONF_SRC="deploy/nginx/$DOMAIN.conf"
|
||||
AVAILABLE="/etc/nginx/sites-available/$DOMAIN"
|
||||
ENABLED="/etc/nginx/sites-enabled/$DOMAIN"
|
||||
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=10
|
||||
-o ControlMaster=auto -o "ControlPath=$HOME/.ssh/kvarta-nginx-%C" -o ControlPersist=60)
|
||||
|
||||
STEP=0
|
||||
step() { STEP=$((STEP + 1)); printf '\n\033[1m[%d] %s\033[0m\n' "$STEP" "$*"; }
|
||||
info() { printf ' %s\n' "$*"; }
|
||||
die() { printf '\033[31mОШИБКА: %s\033[0m\n' "$*" >&2; exit 1; }
|
||||
# shellcheck disable=SC2029 # команды для сервера собираются локально намеренно
|
||||
remote() { ssh "${SSH_OPTS[@]}" "$DEPLOY_HOST" "$@"; }
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
step "Проверка параметров"
|
||||
if [[ -z ${CERTBOT_EMAIL:-} ]]; then
|
||||
die "задайте CERTBOT_EMAIL — на него Let's Encrypt шлёт предупреждения об истечении сертификата:
|
||||
CERTBOT_EMAIL=you@example.com deploy/setup-nginx.sh"
|
||||
fi
|
||||
[[ $CERTBOT_EMAIL =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]] \
|
||||
|| die "CERTBOT_EMAIL не похож на email: $CERTBOT_EMAIL"
|
||||
[[ -f $CONF_SRC ]] || die "нет $CONF_SRC"
|
||||
info "хост $DEPLOY_HOST, домен $DOMAIN, email $CERTBOT_EMAIL"
|
||||
|
||||
step "Проверка сервера"
|
||||
remote true || die "нет ssh-доступа к $DEPLOY_HOST"
|
||||
remote 'command -v nginx >/dev/null && command -v certbot >/dev/null' \
|
||||
|| die "на сервере нет nginx или certbot"
|
||||
# общий nginx должен быть исправен до нас — иначе мы не отличим свою ошибку от чужой
|
||||
remote 'nginx -t 2>&1' >/dev/null || die "nginx -t падает ещё до наших изменений — разберитесь вручную, скрипт ничего не трогал"
|
||||
|
||||
step "Конфиг в $AVAILABLE"
|
||||
# если certbot уже дописал в конфиг 443-блок — не затираем его исходным 80-м
|
||||
if remote "grep -q 'managed by Certbot' $AVAILABLE 2>/dev/null"; then
|
||||
info "конфиг уже обработан certbot — оставляю как есть"
|
||||
else
|
||||
# прежняя версия нашего конфига — чтобы вернуть её, если новая не пройдёт nginx -t
|
||||
remote "[ ! -e $AVAILABLE ] || cp -a $AVAILABLE $AVAILABLE.bak"
|
||||
scp -q "${SSH_OPTS[@]}" "$CONF_SRC" "$DEPLOY_HOST:$AVAILABLE"
|
||||
info "скопирован"
|
||||
fi
|
||||
|
||||
step "Симлинк в sites-enabled"
|
||||
created_link=0
|
||||
if remote "[ -e $ENABLED ] || [ -L $ENABLED ]"; then
|
||||
info "уже есть — не трогаю"
|
||||
else
|
||||
remote "ln -s $AVAILABLE $ENABLED"
|
||||
created_link=1
|
||||
info "создан $ENABLED"
|
||||
fi
|
||||
|
||||
step "nginx -t"
|
||||
if ! remote 'nginx -t'; then
|
||||
if ((created_link)); then
|
||||
remote "rm -f $ENABLED"
|
||||
info "наш симлинк удалён"
|
||||
fi
|
||||
if remote "[ -e $AVAILABLE.bak ]"; then
|
||||
remote "mv -f $AVAILABLE.bak $AVAILABLE"
|
||||
info "восстановлена прежняя версия $AVAILABLE"
|
||||
fi
|
||||
die "конфиг не прошёл nginx -t — reload не выполнен"
|
||||
fi
|
||||
|
||||
remote "rm -f $AVAILABLE.bak"
|
||||
|
||||
step "systemctl reload nginx"
|
||||
remote 'systemctl reload nginx'
|
||||
|
||||
step "Сертификат Let's Encrypt (certbot --nginx)"
|
||||
remote "certbot --nginx -d $DOMAIN --non-interactive --agree-tos --redirect -m '$CERTBOT_EMAIL'"
|
||||
|
||||
step "Итоговая проверка"
|
||||
remote 'nginx -t' && remote "curl -fsS -o /dev/null -w 'https://$DOMAIN/ → %{http_code}\n' https://$DOMAIN/ || true"
|
||||
|
||||
printf '\nГотово: https://%s\n' "$DOMAIN"
|
||||
Reference in New Issue
Block a user