Files
admin/.wiki/sources/vds-kzntsv-bootstrap-2026-05-20.md
vitya 98bcc37d32 import: .wiki/sources/ from MoreThenCms via subtree-split
git-subtree-dir: .wiki/sources
git-subtree-mainline: d25c0577c8
git-subtree-split: a5e96432bc
2026-05-21 13:46:39 +03:00

14 KiB
Raw Permalink Blame History

title, type, tags, ingested, raw_path, updated
title type tags ingested raw_path updated
VDS kzntsv bootstrap session 2026-05-20 source
vds
bootstrap
rusonyx
traefik
portainer
gitea
verdaccio
registry
migration
kreknin
2026-05-20 ../../.tasks/vds-kzntsv-bootstrap.md 2026-05-20

VDS bootstrap session — 2026-05-20

Одна сессия, ~6 часов: активация Rusonyx VDS → 3-фазная подготовка инфра-стека → миграция трёх сервисов (gitea, verdaccio, registry) с восстановленного backup'а на kreknin-synology. Решает первый пункт из future-resilient-architecture-goals — вынос инфра-сервисов с single-point-of-failure хоста. Production CMS остаётся на windows-recovery-host.

Источник истины — .tasks/vds-kzntsv-bootstrap.md. Текущий live-state VDS — vds-kzntsv.

Контекст до сессии

После аварии 2026-05-18 (dead-synology-diskstation развалилась), CMS поднята на windows-recovery-host. Но инфра-сервисы (gitea, verdaccio, docker-registry, owncloud, hermes), которые тоже жили на мёртвой синке, остались только как restored backup на kreknin-synology. User не хочет держать их на windows-recovery-host (тот уже перегружен CMS-стеком + это рабочая машина).

Решение — отдельный облачный VDS у Rusonyx. Заказан 2026-05-19. Активирован 2026-05-20 — IP 89.253.255.94, тариф 160 NVMe (6 vCPU / 8 GB / 160 GB / Ubuntu 24.04).

Хронология

Фаза 0 — pre-flight (DNS + кreds)

User проставил DNS records в REGRU до активации сервера: vds.kzntsv.site, *.vds.kzntsv.site, git.kzntsv.site, verdaccio.kzntsv.site, registry.kzntsv.site → 89.253.255.94. DNS-cut решение: сервисы на kreknin остаются как есть, реальный switch произойдёт после standup'а на VDS (DNS уже там).

Initial креды от Rusonyx: root + одноразовый пароль. Сохранены в ~/projects/.common/secrets/vds-kzntsv.env.

Rusonyx onboarding pain

VNC консоль изначально не открывалась. Помогла кнопка «Остановить VNC» в Управление сервером → Консоль (force-disconnect stale attachment). После этого VNC заработал.

apt update && apt upgrade && apt --fix-broken install && apt upgrade пришлось гонять через VNC (Rusonyx сами рекомендуют: их virt бьёт SSH session, openssh-server restart рвёт connection). По дороге — 5+ dpkg interactive prompts: sshd_config conffile (chose 2 = keep local), cloud.cfg (chose N = keep), grub-pc target disk (1 = /dev/vda whole-disk MBR). Reboot после.

Все эти подробности — в rusonyx-vps-onboarding-quirks.

Phase 1 — bootstrap (через SSH с root + pubkey push)

  1. Pubkey push через plink (PuTTY) с inline -pw (OpenSSH for Windows не поддерживает password в флаге). После push — ssh-key-only login.
  2. Sudo user vitya:Pryakhin10~ + group sudo + NOPASSWD грант (для автоматизации).
  3. sshd harden через drop-in /etc/ssh/sshd_config.d/00-hardening.conf (00- prefix чтобы выиграть first-match precedence над 50-cloud-init.conf который форсит PasswordAuthentication yes). PermitRootLogin no + PasswordAuthentication no.
  4. ufw default-deny + allow 22/80/443 + DB-порты (5432/3306/27017/6379).
  5. fail2ban (default sshd jail).
  6. Docker CE 29.5.1 official APT repo + buildx + compose-plugin. vitya в группу docker.
  7. Hostname vds-kzntsv, timezone Europe/Moscow.
  8. Docker networks proxy + shared-dbs (external).

Phase 1 (cont) — Traefik v2.11 LTS + Portainer 2.21.5

  • Traefik static config: 6 entrypoints (web/websecure/postgres/mariadb/mongo/redis), HTTP-01 LE challenge (DNS уже указан, проходит за 5 sec), dashboard на traefik.vds.kzntsv.site + basicAuth middleware из dynamic file provider.
  • LE issued cert at first request (5 validators с разных AWS regions → 200 OK).

Portainer admin init — 4 итерации. Hard min 12-char policy в Portainer 2.20+ (regression от 2.20.0+), CLI flag --admin-password + bcrypt не bypass'ит policy и фактически выходит broken (bcrypt сохраняется, но login fails). После проб с $2y$/$2a$ prefix, single-quote escape, YAML list form, docker run direct — оказалось CLI flag тупо не работает в 2.21.5. Финал: голый docker run, без --admin-password, потом API admin/init с длинным паролем Pryakhin9-VDS-2026 (18 chars). Деталь — portainer-2.21-admin-password-regression.

API key сгенерирован через /api/users/<id>/tokens, local docker endpoint создан через POST /api/endpoints form-data. Сохранён в vds-kzntsv.env.

Phase 2 — Shared DB park с TLS через traefik

User explicitly: «Я хочу доступ снаружи к базам! Через трафик» — поэтому DBs должны быть accessible from public internet с TLS.

Решение архитектуры — это main lesson:

  • Initial attempt: traefik TCP routers с HostSNI('<db>.vds.kzntsv.site') + tls.passthrough=trueработает для Mongo/Redis (TLS-from-start), не работает для Postgres/MariaDB (STARTTLS-protocols, нет SNI в первых байтах).
  • Switch на HostSNI(*) без tls.* (raw TCP forward) — работает для всех 4 DBs, traefik просто маршрутизирует по entrypoint port'у. DBs терминируют TLS сами.

Подробно в traefik-tcp-passthrough-vs-starttls и db-tls-self-signed-via-traefik-raw-tcp.

Self-signed certs у каждой DB (CN matches <db>.vds.kzntsv.site), strong random hex32 passwords. Postgres alpine использует uid 70 — пришлось перейти на не-alpine postgres:16 (uid 999, matches наш chown). Mongo 7 требует --tlsCAFile (chain of trust enforcement) — добавили self-signed как CA. Все 4 DBs верифицированы через openssl s_client + real protocol probe (pg_dumpall connect, mongosh ping, redis-cli PING, mariadb SELECT VERSION()).

Phase 3.1 — Gitea (миграция от Hyper Backup restored data)

Источник = restored backup на kreknin, не запущенный контейнер. Это была ошибочная гипотеза в начале сессии — потеряли время и обиду user'а («ты почему ни хера не читаешь вики?»). Урок зафиксирован в feedback memory feedback_read_wiki_first.md.

Pipeline:

  1. tar+ssh stream /volume1/docker/gitea/{data,postgres,docker-compose.yml} с kreknin → vds (8m19s, ~2.7G total, ~5.4 Mbps). Sudo на kreknin требовал password (Pryakhin9) — passed через echo Pryakhin9 | sudo -S tar c ... inside ssh quotes.
  2. На VDS — docker run -d --rm postgres:9.6 mounted on restored datadir (postgres uid 999, datadir chown'd, chmod 700). Recovery после non-clean shutdown (last May 9 — последний Hyper Backup snapshot).
  3. pg_dump -U gitea gitea → /tmp/gitea.sql (23 MB, 22400 lines).
  4. Shared postgres 16: CREATE ROLE gitea LOGIN PASSWORD 'gitea'; CREATE DATABASE gitea OWNER gitea ...; через docker exec postgres psql -U postgres -c "..." (peer auth on Unix socket).
  5. Restore psql -U gitea -d gitea < gitea.sql. Gitea автоматически мигрирует schema 9.6 → 16.
  6. Stop temp pg9.6.
  7. Восстановленная data была вложена на уровень глубже (/data/data/git/... вместо /data/git/...) — flatten layout. Gitea на первом старте перезаписал свежий app.ini (env-var driven), нужно было использовать оригинальный app.ini из вложенного data/gitea/conf/app.ini который содержал INSTALL_LOCK=true, LFS_JWT_SECRET=..., SECRET_KEY=... оригинала.
  8. Patch app.ini под VDS: DOMAIN, SSH_DOMAIN, ROOT_URL → git.kzntsv.site; HOST → postgres:5432; SSH_PORT → 2222.
  9. Gitea compose без GITEA__database__* env vars (пусть app.ini рулит).
  10. Smoke: HTTP 200, /api/v1/version{"version":"1.25.5"}, /api/v1/repos/search → 3 first repos (OpeItcLoc03/claude-skills и др.). Final: 132 repos, 4 users.

Phase 3.2 — Verdaccio (file rsync)

  1. rsync storage/ + config/ + plugins/ от kreknin → /opt/stacks/verdaccio/ (~9G transferred, 17m36s, ~8.17 MB/s). Sudo на kreknin не нужен — vitya owns эти файлы.
  2. Chown 10001:65533 (verdaccio uid). Не делать chown -R на parent /opt/stacks/verdaccio — съест permission на root dir, vitya не сможет писать compose. Только sub-dirs.
  3. Compose с image verdaccio/verdaccio:6 + traefik labels → verdaccio.kzntsv.site.
  4. Crashloop — Node 22 в verdaccio:6 требует secret точно 32 chars в /verdaccio/storage/.verdaccio-db.json (не .verdaccio-db как старая версия). Restored secret был 64 chars (старая verdaccio накопила). Fix: openssl rand -hex 16 = 32 chars, overwrite secret в JSON.
  5. User reported: UI пусто. Причина — kreknin'овский config.yaml имеет access: $authenticated для всех packages, и анонимный посетитель не видит ничего. Нужно логиниться (vitya user в restored htpasswd). User'у объяснил, он залогинился — packages появились.

Phase 3.3 — Registry (GC + fresh install)

  1. Registry GC on kreknin локально (mount restored data, run registry:2.8.3 garbage-collect). Mount должен быть PARENT dir, не sub-dir — registry ожидает /var/lib/registry/docker/registry/v2/..., а не /var/lib/registry/registry/v2/.... С правильным mount + -m (modify=delete) flag — 99G → 35G (64G freed).
  2. Запущен rsync 35G с kreknin → /opt/stacks/registry/. ETA ~50 min при 2 MB/s.
  3. Mid-flight user reconsiders: «может зря тащим старые образы? Могу новых наделать». Decision: kill rsync, fresh install. User accepts loss of old images.
  4. Fresh registry с htpasswd (vitya/Pryakhin9), REGISTRY_STORAGE_DELETE_ENABLED=true, CORS headers для UI.
  5. Joxit Registry UI (joxit/docker-registry-ui) на registry-ui.vds.kzntsv.site с DELETE_IMAGES=true для manual cleanup через GUI.

Подробно про GC: registry-gc-mount-and-modify-flag.

Decisions log

  • Tariff 160 NVMe Rusonyx (a не 80 SSD + addon) — operational simplicity overweights ₽1000/мес savings (decision 2026-05-19).
  • Ubuntu 24.04 LTS vs Debian 12 — LTS support до 2029, docker official APT primary target Ubuntu (decision 2026-05-19).
  • Traefik v2.11 LTS vs v3 — user explicit choice (совместимость с windows-recovery-host где v2.6.6).
  • DB access from outside через traefik — user explicit reversal от docker-network-only Q1 answer.
  • DB TLS = self-signed для starts, LE-cert sidecar deferred — operational simplicity.
  • MongoDB include immediately — user explicit.
  • Hermes defer — user explicit.
  • Registry — fresh install, no migration — user mid-flight reversal (могу новых наделать).
  • HostSNI(*) + no tls. для всех DBs* — uniform config работает для всех protocols (STARTTLS + TLS-from-start).
  • Portainer admin pass Pryakhin9-VDS-2026 (18 chars) вместо запрошенного Pryakhin9 (9 chars) — Portainer 2.21+ hard min 12-char policy, --admin-password CLI flag broken в 2.20+. Сохранено в vds-kzntsv.env.

Архитектурное замечание

VDS kzntsv = первый шаг по closing SPOF gap из future-resilient-architecture-goals. Раньше всё крутилось на одной физической коробке (dead-synology-diskstation — упала; windows-recovery-host — тоже SPOF). Теперь инфра-сервисы (git/npm/registry/dbs) живут на отдельном cloud-host'е. Это не полное multi-host resilience: VDS сам по себе тоже single host. Но decouples infra и production CMS, что критично.

Backup pipeline VDS → kreknin (см. follow-up task vds-backup-rsync-kreknin) — следующий шаг.