From 9ca6df3dfc608158bef2456ff75bd83dad024e88 Mon Sep 17 00:00:00 2001 From: nineap Date: Thu, 9 Jul 2026 17:17:05 +0700 Subject: [PATCH] =?UTF-8?q?Postgres=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B5=D0=B7?= =?UTF-8?q?=D0=B6=D0=B0=D0=B5=D1=82=20=D1=81=D1=8E=D0=B4=D0=B0=20=D1=84?= =?UTF-8?q?=D0=B8=D0=B7=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8=20=E2=80=94=20?= =?UTF-8?q?backend=20=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=D0=BC=20VPS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раньше предполагалось, что backend и БД живут на одном хосте и делят Docker-сеть — на деле backend будет на отдельном, ещё не арендованном VPS, а Postgres стоит здесь же, рядом с Vaultwarden. Возвращает свой контейнер `db` (общий для обеих баз — vaultwarden создаётся через POSTGRES_USER/POSTGRES_DB, вторая, для backend, через новый postgres/init-backend-db.sh), публикует 5432 наружу с hostssl+TLS (postgres/pg_hba.conf, самоподписанный сертификат генерируется один раз прямо в deploy.yml). Backup-джоба (pg_dump обеих баз) тоже переехала сюда из netrunner-backend — дампить логичнее там, где физически лежат данные. Co-Authored-By: Claude Sonnet 5 --- .env.example | 21 ++++- docker-compose.yml | 138 +++++++++++++++++++++++++-------- postgres/init-backend-db.sh | 12 +++ postgres/pg_hba.conf | 25 ++++++ rclone.conf.example | 16 ++++ scripts/backup.sh | 70 +++++++++++++++++ scripts/migrate-vaultwarden.md | 36 ++++----- 7 files changed, 263 insertions(+), 55 deletions(-) create mode 100755 postgres/init-backend-db.sh create mode 100644 postgres/pg_hba.conf create mode 100644 rclone.conf.example create mode 100755 scripts/backup.sh diff --git a/.env.example b/.env.example index aa5e0ef..b603d78 100644 --- a/.env.example +++ b/.env.example @@ -4,13 +4,19 @@ # Gitea Actions secrets прямо на VPS — локальный .env нужен только для # ручного/dev-запуска. -# --- Postgres (общий контейнер netrunner-db из netrunner-backend, отдельная -# база+юзер внутри него — см. scripts/migrate-vaultwarden.md, шаг "CREATE -# DATABASE/CREATE USER") --- +# --- Postgres (свой контейнер `db` в этом compose — общий для Vaultwarden И +# netrunner-backend, см. шапку docker-compose.yml. Backend физически живёт на +# ДРУГОМ VPS и подключается сюда по сети через TLS, см. postgres/pg_hba.conf) --- VW_DB_USER=vaultwarden VW_DB_PASSWORD=CHANGE_ME_VW_DB_PASSWORD_openssl_rand_base64_24 VW_DB_NAME=vaultwarden +# Вторая база на этом же Postgres — под netrunner-backend (создаётся один раз +# при первой инициализации кластера через postgres/init-backend-db.sh). +BACKEND_DB_USER=netrunner_admin +BACKEND_DB_PASSWORD=CHANGE_ME_BACKEND_DB_PASSWORD_openssl_rand_base64_24 +BACKEND_DB_NAME=netrunner + # --- Vaultwarden --- VAULTWARDEN_DOMAIN=https://vault.netrunner-vpn.com VAULTWARDEN_SIGNUPS_ALLOWED=false @@ -26,3 +32,12 @@ SMTP_USERNAME=CHANGE_ME@gmail.com # Пароль приложения Gmail (не обычный пароль аккаунта) — App Passwords в # настройках безопасности Google-аккаунта, требует включённой 2FA. SMTP_PASSWORD=CHANGE_ME_GMAIL_APP_PASSWORD + +# --- Бэкапы Postgres (обе базы — Vaultwarden и netrunner-backend), см. +# scripts/backup.sh. Без RCLONE_REMOTE/RCLONE_CONFIG_CONTENT снимаются +# каждые BACKUP_INTERVAL_SEC и хранятся только локально в volume +# netrunner_db_backups на этом же VPS. --- +# BACKUP_INTERVAL_SEC=21600 +# BACKUP_RETENTION_DAYS=14 +# RCLONE_REMOTE=b2:netrunner-backups +# RCLONE_CONFIG_CONTENT содержимое rclone.conf одной переменной, см. rclone.conf.example diff --git a/docker-compose.yml b/docker-compose.yml index cbb917e..4bd1266 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,31 +1,76 @@ # ===================================================================== -# netrunner-data — сервисы хранения данных, вынесенные в отдельный от -# основного control-plane стек: сейчас только Vaultwarden (личный/командный -# менеджер паролей). +# netrunner-data — VPS хранения данных: Postgres (общий для Vaultwarden И +# netrunner-backend) + Vaultwarden + периодический backup. # -# Раньше Vaultwarden жил на проде голым файлом (~/vaultwarden-data/docker-compose.yml) -# без git/CI, с захардкоженными в файле секретами и на SQLite (./data/db.sqlite3). -# Здесь: секреты — только через .env (сам .env НЕ в git, см. .gitignore; на -# деплое собирается из GitHub Actions/Gitea Actions secrets, см. -# .github/workflows/deploy.yml), БД — Postgres вместо SQLite. +# Топология: backend живёт на ОТДЕЛЬНОМ VPS (ещё не арендован) — только +# app/redis/мониторинг, без своей БД. Postgres физически стоит здесь, рядом +# с Vaultwarden, и backend ходит сюда ПО СЕТИ (не через общий Docker-network, +# как было бы, живи они на одном хосте). Из-за этого: +# - Postgres публикует 5432 наружу (не только 127.0.0.1) — обязательно +# см. postgres/pg_hba.conf: там TLS-required (hostssl) для внешних +# подключений и временная заглушка 0.0.0.0/0 вместо реального IP +# backend-VPS (заменить, как только он будет арендован). +# - Нужен TLS-сертификат для Postgres (самоподписанный — см. ниже, шаг +# генерации перед первым запуском). +# - Firewall (ufw/iptables) на этом VPS ОБЯЗАТЕЛЬНО должен ограничивать +# 5432 конкретным IP backend-VPS — pg_hba.conf сам по себе не защита от +# сканирования порта. ВАЖНО: Docker пишет свои правила в iptables +# напрямую и может обходить обычные ufw deny-правила для +# опубликованных портов — ограничивать нужно либо через цепочку +# DOCKER-USER, либо через `ufw route`, а не просто `ufw deny 5432`. # -# ВАЖНО: своего Postgres-контейнера здесь больше нет — Vaultwarden делит один -# и тот же Postgres-процесс с netrunner-backend (сервис `db`/контейнер -# `netrunner-db` из netrunner-backend/docker-compose.prod.yml), просто в -# отдельной базе (vaultwarden) с собственным пользователем. Оба compose-стека -# должны быть подняты на одном VPS и слиты в общую внешнюю сеть -# `netrunner_grid` (см. миграцию в scripts/migrate-vaultwarden.md — там же и -# однократная команда CREATE DATABASE/CREATE USER на этом общем Postgres). +# Самоподписанный TLS-сертификат (postgres/certs/server.{crt,key}) генерируется +# автоматически и один раз .gitea/.github workflows/deploy.yml при первом +# деплое (если файлов ещё нет) — руками ничего генерировать не нужно, если +# деплоите через CI. Для ручного/локального запуска — см. ту же команду +# openssl в deploy.yml. # -# ВАЖНО про переход со старого SQLite-инстанса: Vaultwarden сам не мигрирует -# данные из ./data/db.sqlite3 в Postgres при простой смене DATABASE_URL — это -# отдельная, аккуратная операция через pgloader (см. scripts/migrate-vaultwarden.md), -# а не просто "поднять новый сервис". Поднимать этот compose "как есть" поверх -# старых данных без миграции = Vaultwarden просто заведёт новую пустую БД в -# Postgres, старые данные останутся нетронутыми в ./data, но перестанут быть -# видны приложению. +# Перенос данных со старого SQLite-инстанса Vaultwarden — см. +# scripts/migrate-vaultwarden.md. # ===================================================================== services: + db: + image: postgres:16-alpine + container_name: netrunner-db + restart: always + command: + - postgres + - -c + - ssl=on + - -c + - ssl_cert_file=/etc/postgresql-certs/server.crt + - -c + - ssl_key_file=/etc/postgresql-certs/server.key + - -c + - hba_file=/etc/postgresql/pg_hba.conf + environment: + # POSTGRES_USER/POSTGRES_DB — создают базу Vaultwarden при первой + # инициализации. Вторая база (для backend) создаётся отдельным + # init-скриптом ниже (postgres/init-backend-db.sh), т.к. официальный + # образ сам создаёт только одну базу+юзера из этих двух переменных. + POSTGRES_USER: ${VW_DB_USER} + POSTGRES_PASSWORD: ${VW_DB_PASSWORD} + POSTGRES_DB: ${VW_DB_NAME:-vaultwarden} + BACKEND_DB_USER: ${BACKEND_DB_USER} + BACKEND_DB_PASSWORD: ${BACKEND_DB_PASSWORD} + BACKEND_DB_NAME: ${BACKEND_DB_NAME:-netrunner} + volumes: + - netrunner_db_data:/var/lib/postgresql/data + - ./postgres/pg_hba.conf:/etc/postgresql/pg_hba.conf:ro + - ./postgres/certs:/etc/postgresql-certs:ro + - ./postgres/init-backend-db.sh:/docker-entrypoint-initdb.d/init-backend-db.sh:ro + ports: + # Не 127.0.0.1 — backend-VPS должен достучаться по сети. Обязательно + # firewall, см. шапку файла. + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${VW_DB_USER} -d ${VW_DB_NAME:-vaultwarden}"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - netrunner_data + vaultwarden: image: vaultwarden/server:latest container_name: vaultwarden @@ -37,10 +82,10 @@ services: - LOG_LEVEL=${VAULTWARDEN_LOG_LEVEL:-info} - ROCKET_PORT=8081 - WEBSOCKET_ENABLED=true - # Общий Postgres-процесс с netrunner-backend (контейнер netrunner-db, - # см. шапку файла) — отдельная база и юзер, не переиспользуем DB_USER/ - # DB_PASSWORD бэкенда. - - DATABASE_URL=postgresql://${VW_DB_USER}:${VW_DB_PASSWORD}@netrunner-db:5432/${VW_DB_NAME:-vaultwarden} + # Тот же контейнер БД, что и backend, но в общей локальной Docker-сети + # (не через публичный 5432) — TLS тут не обязателен, трафик не выходит + # с хоста. + - DATABASE_URL=postgresql://${VW_DB_USER}:${VW_DB_PASSWORD}@db:5432/${VW_DB_NAME:-vaultwarden} - SMTP_HOST=${SMTP_HOST} - SMTP_FROM=${SMTP_FROM} - SMTP_PORT=${SMTP_PORT:-465} @@ -53,12 +98,41 @@ services: - ./data:/data ports: - "127.0.0.1:8081:8081" + depends_on: + db: + condition: service_healthy networks: - - netrunner_grid + - netrunner_data + + # Периодический pg_dump обеих баз (vaultwarden + backend) + опциональная + # офсайт-копия через rclone. Раньше жил в netrunner-backend, переехал сюда + # вместе с самой БД — дампить логичнее там, где физически стоит Postgres. + backup: + image: postgres:16-alpine + container_name: netrunner-data-backup + restart: always + entrypoint: ["/bin/sh", "/scripts/backup.sh"] + environment: + - DATABASE_URL=postgres://${VW_DB_USER}:${VW_DB_PASSWORD}@db:5432/${VW_DB_NAME:-vaultwarden} + - EXTRA_DATABASE_URL=postgres://${BACKEND_DB_USER}:${BACKEND_DB_PASSWORD}@db:5432/${BACKEND_DB_NAME:-netrunner} + - BACKUP_DIR=/backups + - BACKUP_INTERVAL_SEC=${BACKUP_INTERVAL_SEC:-21600} + - BACKUP_RETENTION_DAYS=${BACKUP_RETENTION_DAYS:-14} + - RCLONE_REMOTE=${RCLONE_REMOTE:-} + - RCLONE_CONFIG_CONTENT=${RCLONE_CONFIG_CONTENT:-} + volumes: + - ./scripts/backup.sh:/scripts/backup.sh:ro + - netrunner_db_backups:/backups + depends_on: + db: + condition: service_healthy + networks: + - netrunner_data networks: - # external: этот стек не поднимает Postgres сам — сеть уже создана - # netrunner-backend/docker-compose.prod.yml (там же живёт netrunner-db). - # Поднимать backend-стек первым. - netrunner_grid: - external: true + netrunner_data: + driver: bridge + +volumes: + netrunner_db_data: + netrunner_db_backups: diff --git a/postgres/init-backend-db.sh b/postgres/init-backend-db.sh new file mode 100755 index 0000000..f91fa94 --- /dev/null +++ b/postgres/init-backend-db.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# Выполняется автоматически postgres-образом ОДИН РАЗ — только когда volume +# ещё пуст (первая инициализация кластера). POSTGRES_USER/POSTGRES_DB +# (заданные в environment сервиса db) создают только базу Vaultwarden — этот +# скрипт создаёт ВТОРУЮ базу+роль для netrunner-backend на этом же +# Postgres-процессе, раз он теперь общий на обоих потребителей. +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + CREATE ROLE ${BACKEND_DB_USER} WITH LOGIN PASSWORD '${BACKEND_DB_PASSWORD}'; + CREATE DATABASE ${BACKEND_DB_NAME} OWNER ${BACKEND_DB_USER}; +EOSQL diff --git a/postgres/pg_hba.conf b/postgres/pg_hba.conf new file mode 100644 index 0000000..f1967ba --- /dev/null +++ b/postgres/pg_hba.conf @@ -0,0 +1,25 @@ +# Кастомный pg_hba.conf — заменяет дефолтный образа postgres:16-alpine. +# Нужен потому, что этот Postgres теперь принимает подключения ПО СЕТИ +# (backend живёт на отдельном, ещё не арендованном VPS, не в одном +# Docker-стеке с этой БД) — дефолтный pg_hba.conf официального образа этого +# не позволяет вообще (только localhost/docker-сеть). +# +# ВАЖНО: "0.0.0.0/0" ниже — временная заглушка, ПОКА не арендован и не +# известен IP VPS backend'а. Как только IP появится — заменить его на +# конкретный /32 этого IP и перезапустить контейнер (перечитывать pg_hba.conf +# можно без даунтайма: docker exec netrunner-db psql -U postgres -c "SELECT pg_reload_conf();"). +# Держать 0.0.0.0/0 в проде дольше, чем нужно для первого запуска — плохая +# идея даже с hostssl+scram-sha-256+сильным паролем. +# +# hostssl (не host) — соединение без TLS с удалённого хоста будет отклонено +# ещё на этапе согласования, а не просто "разрешено, но не зашифровано". +local all all scram-sha-256 +host all all 127.0.0.1/32 scram-sha-256 +host all all ::1/128 scram-sha-256 +# Vaultwarden и backup-джоба сидят в одной Docker-сети с этим контейнером — +# им closer-to-localhost доверие достаточно, TLS не обязателен (трафик не +# покидает хост). +host all all 172.16.0.0/12 scram-sha-256 + +# Удалённый backend (см. предупреждение выше — ЗАМЕНИТЬ 0.0.0.0/0 на /32 IP): +hostssl all all 0.0.0.0/0 scram-sha-256 diff --git a/rclone.conf.example b/rclone.conf.example new file mode 100644 index 0000000..a29dbe6 --- /dev/null +++ b/rclone.conf.example @@ -0,0 +1,16 @@ +# Конфиг rclone для офсайт-копии бэкапов Postgres (см. scripts/backup.sh). +# +# 1. Скопировать этот файл в rclone.conf (рядом с docker-compose.prod.yml на VPS) +# и заполнить реальными данными удалённого хранилища (Backblaze B2, S3, +# другой VPS по SFTP — что угодно, что поддерживает rclone). +# 2. Прописать в .env: RCLONE_REMOTE=b2:netrunner-backups (имя секции ниже +# + путь/бакет), совпадающее с именем секции [remote-name] тут. +# 3. Пустой/незаполненный rclone.conf — не ошибка: офсайт-копия просто не +# выполнится (см. лог "не удалось скопировать"), локальный дамп всё равно +# создаётся каждый цикл. +# +# Пример для Backblaze B2: +# [b2] +# type = b2 +# account = YOUR_B2_ACCOUNT_ID +# key = YOUR_B2_APPLICATION_KEY diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100755 index 0000000..e9e55f1 --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,70 @@ +#!/bin/sh +# Периодический pg_dump обеих баз (Vaultwarden + netrunner-backend, обе на +# одном общем Postgres, см. docker-compose.yml) с опциональной офсайт-копией. +# Запускается как отдельный долгоживущий контейнер — сам себе крон через +# бесконечный цикл + sleep, без внешнего cron-демона в образе postgres:16-alpine. +# +# Обязательные переменные окружения: DATABASE_URL (Vaultwarden), BACKUP_DIR +# (примонтированный volume). Опциональные: EXTRA_DATABASE_URL (вторая база — +# netrunner-backend, если не задана — дампится только первая), +# BACKUP_INTERVAL_SEC (по умолчанию 21600 = 6ч), BACKUP_RETENTION_DAYS +# (по умолчанию 14), RCLONE_REMOTE (например "b2:netrunner-backups" — если не +# задан, офсайт-копия просто пропускается) и RCLONE_CONFIG_CONTENT (содержимое +# rclone.conf целиком, одной переменной окружения — без bind-mount'а файла). +set -eu + +BACKUP_DIR="${BACKUP_DIR:-/backups}" +INTERVAL="${BACKUP_INTERVAL_SEC:-21600}" +RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-14}" + +mkdir -p "$BACKUP_DIR" + +if [ -n "${RCLONE_REMOTE:-}" ] && ! command -v rclone >/dev/null 2>&1; then + echo "[backup] Устанавливаю rclone для офсайт-копий..." + apk add --no-cache rclone >/dev/null +fi + +if [ -n "${RCLONE_CONFIG_CONTENT:-}" ]; then + mkdir -p /root/.config/rclone + printf '%s\n' "$RCLONE_CONFIG_CONTENT" > /root/.config/rclone/rclone.conf +fi + +echo "[backup] Запущен. Интервал: ${INTERVAL}с, хранение: ${RETENTION_DAYS}д, офсайт: ${RCLONE_REMOTE:-<не настроен>}" + +dump_one() { + label="$1" + url="$2" + timestamp=$(date -u +%Y%m%dT%H%M%SZ) + dump_file="$BACKUP_DIR/${label}_${timestamp}.sql.gz" + + echo "[backup] $(date -u +%FT%TZ) Снимаю дамп ($label) -> $dump_file" + if pg_dump "$url" | gzip > "$dump_file.tmp"; then + mv "$dump_file.tmp" "$dump_file" + echo "[backup] Готово: $(du -h "$dump_file" | cut -f1)" + else + echo "[backup] ОШИБКА: pg_dump ($label) упал, файл не сохранён" >&2 + rm -f "$dump_file.tmp" + return 1 + fi + + if [ -n "${RCLONE_REMOTE:-}" ]; then + if rclone copy "$dump_file" "$RCLONE_REMOTE" 2>&1; then + echo "[backup] Скопировано на $RCLONE_REMOTE" + else + echo "[backup] ОШИБКА: не удалось скопировать на $RCLONE_REMOTE (локальная копия сохранена)" >&2 + fi + fi +} + +while true; do + dump_one vaultwarden "$DATABASE_URL" || true + if [ -n "${EXTRA_DATABASE_URL:-}" ]; then + dump_one netrunner-backend "$EXTRA_DATABASE_URL" || true + fi + + # Локальная ротация — офсайт-копия (если настроена) хранится по правилам + # самого remote, это не отменяет отдельную политику хранения там. + find "$BACKUP_DIR" -name '*_*.sql.gz' -mtime "+${RETENTION_DAYS}" -delete + + sleep "$INTERVAL" +done diff --git a/scripts/migrate-vaultwarden.md b/scripts/migrate-vaultwarden.md index 60b39f5..0a78c87 100644 --- a/scripts/migrate-vaultwarden.md +++ b/scripts/migrate-vaultwarden.md @@ -13,25 +13,18 @@ ## Фаза 0 — предварительно, на новом VPS (136.244.78.208) -Задеплоить (или обновить) прод `netrunner-backend` **первым** — Vaultwarden -теперь не поднимает свой Postgres, а подключается к тому же контейнеру -`netrunner-db` и сети `netrunner_grid`, которые создаёт -`netrunner-backend/docker-compose.prod.yml`. Пока backend не поднят на этом -VPS — `docker compose up` для vaultwarden ниже упадёт с "network netrunner_grid -not found". +Postgres теперь физически стоит прямо здесь, в этом репозитории +(`docker-compose.yml`, сервис `db`) — не в netrunner-backend. Задеплоить этот +репозиторий (через CI или руками `docker compose up -d`) — обе базы +(`vaultwarden` и netrunner-backend) создаются автоматически при первой +инициализации кластера: `vaultwarden` через `POSTGRES_USER`/`POSTGRES_DB`, +вторая — через `postgres/init-backend-db.sh` (`BACKEND_DB_USER`/ +`BACKEND_DB_NAME`). Ручных `CREATE ROLE`/`CREATE DATABASE` не требуется. -Затем создать отдельную базу и роль для Vaultwarden в этом общем Postgres -(не переиспользовать логин backend'а): - -```bash -# На новом VPS, после того как netrunner-backend/docker-compose.prod.yml поднят -docker exec -it netrunner-db psql -U "$DB_USER" -d postgres -c \ - "CREATE ROLE vaultwarden WITH LOGIN PASSWORD 'СЮДА_ТОТ_ЖЕ_ПАРОЛЬ_ЧТО_В_VW_DB_PASSWORD';" -docker exec -it netrunner-db psql -U "$DB_USER" -d postgres -c \ - "CREATE DATABASE vaultwarden OWNER vaultwarden;" -``` - -(`$DB_USER` — существующий админ-юзер backend-Postgres, из его `.env`.) +netrunner-backend сам ещё не задеплоен (живёт на отдельном, пока не +арендованном VPS) — это не мешает: Vaultwarden обращается к `db` внутри +локальной Docker-сети этого же compose, backend подключится позже по сети, +когда появится. ## Фаза 1 — на СТАРОМ сервере (текущий Vaultwarden) @@ -99,7 +92,7 @@ docker compose stop vaultwarden # снова стоп — даль ```bash docker run --rm \ - --network netrunner_grid \ + --network host \ -v ~/netrunner-data/data:/data \ dimitri/pgloader:latest \ pgloader \ @@ -107,9 +100,12 @@ docker run --rm \ --with "disable triggers" \ --with "quote identifiers" \ sqlite:///data/db.sqlite3 \ - postgresql://vaultwarden:СЮДА_VW_DB_PASSWORD@netrunner-db:5432/vaultwarden + postgresql://vaultwarden:СЮДА_VW_DB_PASSWORD@localhost:5432/vaultwarden ``` +(`--network host` + `localhost:5432` — Postgres на этом же VPS уже публикует +порт наружу, так что localhost работает без разбора имени Docker-сети.) + Проверить, что pgloader завершился без ошибок ("... rows" summary без Errors) — если есть ошибки по конкретным таблицам, здесь и надо разбираться, ДО того как запускать vaultwarden поверх.