From d0defa8e8e2feab541e76c7c44961b6b5cfdcc10 Mon Sep 17 00:00:00 2001 From: vitya Date: Wed, 20 May 2026 22:39:31 +0300 Subject: [PATCH] =?UTF-8?q?docs(.wiki,.tasks):=20vds-kzntsv-bootstrap=20do?= =?UTF-8?q?ne=20=E2=80=94=203=20phases=20shipped?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VDS Rusonyx 160 NVMe (89.253.255.94 / vds.kzntsv.site) активирован 2026-05-20. 3 фазы за ~6 часов: - Phase 1: harden + docker 29.5.1 + traefik v2.11 + portainer 2.21.5 - Phase 2: shared DB park (postgres 16 / mariadb 11.4 / mongo 7 / redis 7) via traefik raw TCP forward + self-signed TLS - Phase 3: миграция gitea (132 repos), verdaccio (2063 pkgs), registry fresh install (user accepted loss old images) + Joxit GUI Ingest 1 source + 1 entity (vds-kzntsv) + 6 concepts (traefik-tcp- passthrough-vs-starttls / portainer-2.21-admin-password-regression / db-tls-self-signed-via-traefik-raw-tcp / rusonyx-vps-onboarding-quirks / registry-gc-mount-and-modify-flag / compose-bcrypt-escape-trap). 3 follow-up ⚪ tasks: vds-gc-cron, vds-backup-rsync-kreknin, vds-ntfy-push. Co-Authored-By: Claude Opus 4.7 (1M context) --- vds-kzntsv-bootstrap-2026-05-20.md | 121 +++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 vds-kzntsv-bootstrap-2026-05-20.md diff --git a/vds-kzntsv-bootstrap-2026-05-20.md b/vds-kzntsv-bootstrap-2026-05-20.md new file mode 100644 index 0000000..7e02eb8 --- /dev/null +++ b/vds-kzntsv-bootstrap-2026-05-20.md @@ -0,0 +1,121 @@ +--- +title: VDS kzntsv bootstrap session 2026-05-20 +type: source +tags: [vds, bootstrap, rusonyx, traefik, portainer, gitea, verdaccio, registry, migration, kreknin] +ingested: 2026-05-20 +raw_path: ../../.tasks/vds-kzntsv-bootstrap.md +updated: 2026-05-20 +--- + +# VDS bootstrap session — 2026-05-20 + +Одна сессия, ~6 часов: активация Rusonyx VDS → 3-фазная подготовка инфра-стека → миграция трёх сервисов (gitea, verdaccio, registry) с восстановленного backup'а на [`kreknin-synology`](../entities/kreknin-synology.md). Решает первый пункт из [`future-resilient-architecture-goals`](../concepts/future-resilient-architecture-goals.md) — вынос инфра-сервисов с single-point-of-failure хоста. Production CMS остаётся на [`windows-recovery-host`](../entities/windows-recovery-host.md). + +Источник истины — `.tasks/vds-kzntsv-bootstrap.md`. Текущий live-state VDS — [`vds-kzntsv`](../entities/vds-kzntsv.md). + +## Контекст до сессии + +После аварии 2026-05-18 ([`dead-synology-diskstation`](../entities/dead-synology-diskstation.md) развалилась), CMS поднята на [`windows-recovery-host`](../entities/windows-recovery-host.md). Но инфра-сервисы (gitea, verdaccio, docker-registry, owncloud, hermes), которые тоже жили на мёртвой синке, остались только как restored backup на [`kreknin-synology`](../entities/kreknin-synology.md). 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`](../concepts/rusonyx-vps-onboarding-quirks.md). + +### 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`](../concepts/portainer-2.21-admin-password-regression.md). + +API key сгенерирован через `/api/users//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('.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`](../concepts/traefik-tcp-passthrough-vs-starttls.md) и [`db-tls-self-signed-via-traefik-raw-tcp`](../concepts/db-tls-self-signed-via-traefik-raw-tcp.md). + +Self-signed certs у каждой DB (CN matches `.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`](../concepts/registry-gc-mount-and-modify-flag.md). + +## 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`](../concepts/future-resilient-architecture-goals.md). Раньше всё крутилось на одной физической коробке ([`dead-synology-diskstation`](../entities/dead-synology-diskstation.md) — упала; [`windows-recovery-host`](../entities/windows-recovery-host.md) — тоже 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`](../../.tasks/vds-backup-rsync-kreknin.md)) — следующий шаг.