Перейти к содержанию

51-storage-c: развёртывание ragserver storage backend

Дата: 11.08.2026 (обновлено 14.08.2026 — деплой Шагов 1 и 2 выполнен) Статус: Сервис oswold1979/alatyr-storage-api запущен на ragserver, публичный HTTPS https://storage-api.alatyr-service.ru работает. Осталось: merge PR #11, env alatyr-service в проде, E2E QA. Связано: PR #11, alatyr-storage-api, ragserver-workstation.md, nginx-vps-configs.md.


Итоговая архитектура

Изначально планировали VPS → AmneziaWG-туннель → ragserver (по аналогии с ошибочным пониманием rag-api). На деле выяснилось, что ragserver уже доступен через белый IP роутера ASUS RT-N12E (176.99.153.164), с пробросом 80/tcp и 443/tcp192.168.1.200. Storage-api идёт по тому же паттерну что rag.alatyr-service.ruVPS в цепочке не участвует:

alatyr-service.ru клиент
    ↓ HTTPS
storage-api.alatyr-service.ru → DNS A → 176.99.153.164 (роутер ASUS)
    ↓ NAT :443
192.168.1.200:443 (ragserver nginx)
    ↓ proxy_pass
10.8.1.12:3100 (docker контейнер alatyr-storage-api)
    ↓ bind-mount
/opt/alatyr-storage/ (файлы на диске ragserver, права 600 oswold:oswold)

Плюсы этой схемы: - Один hop меньше, ниже задержка (важно для 500MB uploads) - Меньше точек отказа (VPS-nginx не участвует) - Не зависит от состояния AmneziaWG-туннеля VPS↔ragserver - Uniform pattern с rag.alatyr-service.ru

Точка отказа: ragserver должен быть онлайн. Роутер должен быть онлайн. Провайдер офиса (Инетком) не должен блокировать. Это уже принятая архитектурная зависимость всей RAG-платформы.


Шаг 1 · Ragserver: развернуть сервис — ВЫПОЛНЕНО 14.08.2026

1.1 SSH deploy key для приватного репо

Приватный репозиторий, поэтому ragserver'у нужен ключ. По паттерну id_ed25519_alatyr_kb (см. rag-sync.md):

# На ragserver под oswold (sudo не нужен — пишем в свой ~/.ssh/):
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_alatyr_storage_api \
  -C "ragserver-alatyr-storage-api-deploy-key" -N ""

cat ~/.ssh/id_ed25519_alatyr_storage_api.pub
# → скопировать в GitHub UI: Settings → Deploy Keys → Add
# https://github.com/oswold1979/alatyr-storage-api/settings/keys/new
# Title: ragserver-alatyr-storage-api-deploy-key
# Allow write access: НЕТ (read-only)

cat >> ~/.ssh/config <<'EOF'

# Deploy key для alatyr-storage-api (read-only)
Host github-alatyr-storage-api
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_alatyr_storage_api
    IdentitiesOnly yes
EOF
chmod 600 ~/.ssh/config

# Проверка
ssh -T git@github-alatyr-storage-api
# Hi oswold1979/alatyr-storage-api! You've successfully authenticated...

1.2 Директории и клонирование

sudo mkdir -p /opt/alatyr-storage-api /opt/alatyr-storage
sudo chown -R oswold:oswold /opt/alatyr-storage-api
sudo chown -R 1000:1000 /opt/alatyr-storage
sudo chmod 700 /opt/alatyr-storage

cd /opt/alatyr-storage-api
git clone git@github-alatyr-storage-api:oswold1979/alatyr-storage-api.git .

1.3 Токен и запуск

# Bearer-токен (сгенерирован 14.08.2026, лежит в Vaultwarden как
# "STORAGE_API_TOKEN (alatyr-storage-api)")
echo "STORAGE_API_TOKEN=<из Vaultwarden>" > .env
chmod 600 .env

docker compose up -d --build
docker compose ps
# alatyr-storage-api  Up (healthy)  10.8.1.12:3100->3100/tcp

docker compose logs --tail 5
# alatyr-storage-api listening on 0.0.0.0:3100, storage=/data, maxUpload=500MB

1.4 Round-trip QA (локально) — ВЫПОЛНЕНО

TOKEN=$(grep STORAGE_API_TOKEN .env | cut -d= -f2)

curl -s http://10.8.1.12:3100/health
# {"ok":true,"storageRoot":"/data","timestamp":"..."}

echo "hello" | curl -s -X PUT -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: text/plain" --data-binary @- \
  http://10.8.1.12:3100/files/smoke/test.txt
# {"ok":true,"size":6}

curl -s -H "Authorization: Bearer $TOKEN" \
  http://10.8.1.12:3100/files/smoke/test.txt
# hello

curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
  http://10.8.1.12:3100/files/smoke/test.txt -w "HTTP %{http_code}\n"
# HTTP 204

Все 4 теста прошли. Порт 10.8.1.12:3100 опубликован Docker на локальном AmneziaWG-адресе хоста. Публичный вход идёт через домашний роутер и nginx на ragserver, но локальный nginx проксирует запрос к этому VPN-bound порту, поэтому интерфейс awg-rag должен быть поднят.


Шаг 2 · nginx + certbot на ragserver — ВЫПОЛНЕНО 14.08.2026

2.1 DNS в reg.ru

storage-api.alatyr-service.ru  A  176.99.153.164

Пропагация ~5-10 минут для основных резолверов.

2.2 nginx-конфиг

sudo tee /etc/nginx/sites-available/storage-api.alatyr-service.ru > /dev/null <<'NGINX'
# alatyr-storage-api reverse proxy
# HTTP only — certbot will add HTTPS on top

upstream storage_api_upstream {
    server 10.8.1.12:3100;
}

server {
    server_name storage-api.alatyr-service.ru;

    access_log /var/log/nginx/storage-api.alatyr-service.ru-access.log;
    error_log  /var/log/nginx/storage-api.alatyr-service.ru-error.log;

    client_max_body_size 500M;

    proxy_connect_timeout   60s;
    proxy_send_timeout      600s;
    proxy_read_timeout      600s;

    location / {
        proxy_pass http://storage_api_upstream;

        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_set_header X-Forwarded-Host  $host;

        proxy_request_buffering off;
        proxy_buffering off;
    }
}
NGINX

sudo ln -sf /etc/nginx/sites-available/storage-api.alatyr-service.ru \
           /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

2.3 certbot (HTTP-01 challenge через белый IP роутера)

sudo certbot --nginx -d storage-api.alatyr-service.ru \
  --agree-tos --non-interactive \
  --email oswold1979@gmail.com --redirect

Certbot автоматически добавит listen 443 ssl, HTTP→HTTPS редирект, SSL-параметры. Сертификат — /etc/letsencrypt/live/storage-api.alatyr-service.ru/, автообновление настроено systemd-таймером certbot.timer (см. certbot.md).

2.4 E2E QA снаружи — ВЫПОЛНЕНО

# Из Perplexity sandbox (внешняя сеть):
curl -s https://storage-api.alatyr-service.ru/health
# {"ok":true,"storageRoot":"/data","timestamp":"..."}

TOKEN=<из Vaultwarden>

echo "e2e-test" | curl -s -X PUT -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: text/plain" --data-binary @- \
  https://storage-api.alatyr-service.ru/files/e2e/from-sandbox.txt
# {"ok":true,"size":22}

curl -s -H "Authorization: Bearer $TOKEN" \
  https://storage-api.alatyr-service.ru/files/e2e/from-sandbox.txt
# e2e-test-from-sandbox

curl -sI http://storage-api.alatyr-service.ru/health
# HTTP/1.1 301 Moved Permanently (→ HTTPS)

curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
  https://storage-api.alatyr-service.ru/files/e2e/from-sandbox.txt \
  -w "HTTP %{http_code}\n"
# HTTP 204

Все тесты прошли.


Шаг 3 · alatyr-service: env + merge PR — СДЕЛАНО (14.08.2026)

3.1 Merge PR #11

github.com/oswold1979/alatyr-service/pull/11 — добавляет RagserverAdapter в server/file-storage/. Проверить: - CI зелёный - Никаких конфликтов с main

3.2 Env на VPS

В /etc/alatyr-service.env (или где хранится prod env, см. nginx-vps-configs.md):

FILE_STORAGE_BACKEND=ragserver
FILE_STORAGE_URL=https://storage-api.alatyr-service.ru
FILE_STORAGE_TOKEN=<тот же токен что в /opt/alatyr-storage-api/.env на ragserver>

Затем sudo systemctl restart alatyr-service.service (или как называется unit).

3.3 Prod E2E QA

  1. Открыть карточку любого объекта в admin-панели
  2. Загрузить тестовый файл (небольшой PDF или txt)
  3. Проверить что файл появился в /opt/alatyr-storage/objects/<slug>/ на ragserver
  4. Скачать через UI — контент совпадает
  5. Удалить — файл исчезает с диска

Если E2E ok — задача 51-storage-c закрыта, идём в 51b/51c/51d/51e.

3.4 Фактическое закрытие 14.08.2026

Шаги 3.1 + 3.2 выполнены утром 14.08: - PR #11 merged в main - /etc/alatyr-service.env обновлён: FILE_STORAGE_BACKEND=ragserver, FILE_STORAGE_RAGSERVER_URL=https://storage-api.alatyr-service.ru, STORAGE_API_TOKEN=<hex64> - sudo systemctl restart alatyr-service — OK

Шаг 3.3 — первая попытка E2E вышла с ошибкой RagserverAdapter: сетевая ошибка: fetch failed в UI. Диагностика:

$ curl -v https://storage-api.alatyr-service.ru/  # на VPS
> Сертификат не матчится с hostname — SSL SAN mismatch

$ getent hosts storage-api.alatyr-service.ru
46.17.99.183 storage-api.alatyr-service.ru   # ⚠️ сам VPS!

$ resolvectl status ens1
DNS Servers: 8.8.8.8   # Google DNS держал старую A-запись

Временный фикс на живую, выполненный 14.08.2026:

sudo resolvectl dns ens1 1.1.1.1 77.88.8.8
sudo resolvectl flush-caches
getent hosts storage-api.alatyr-service.ru   # → 176.99.153.164 ✅

Постоянный фикс выполнен 25.08.2026, задача 54 закрыта. Создан /etc/netplan/99-custom-dns.yaml с DNS 1.1.1.1, 77.88.8.8 и dhcp4-overrides/dhcp6-overrides: use-dns: false. Конфигурация прошла netplan generate, применена через netplan try; networkd-файл содержит оба DNS= и UseDNS=false. После применения домен разрешается в 176.99.153.164, /health возвращает HTTP 200.

Повторный E2E через UI — вечером: - PUT /files/260811--skudsdf/01-договор/основной/<uuid>.docx → 201 (2ms) — логи storage-api - Файл на диске: /opt/alatyr-storage/260811--skudsdf/01-договор/основной/<uuid>.docx, 26105 байт, 600 oswold - Карточка договора в UI показывает файл, скачивание работает

Но: имя скачанного файла было Ñ_енÑ_мµ.docx вместо Резюме.docx — кракозябра. Корень: multer/busboy отдавал req.file.originalname в latin1, плюс Content-Disposition без filename*=UTF-8''.... Закрыто PR #23 (c86935d → squash-merge 5b7c9c4), две helper-функции decodeMulterFilename() + contentDispositionAttachment() в server/routes.ts, применено в 5 местах (upload/download documents+contracts, download estimate.pdf). GHA deploy 31824065325 успешен ~1.5 мин.

Прод E2E после фикса кириллицы: пользователь удалил два битых договора через UI, залил заново с русским именем → в карточке и при скачивании — корректное Резюме.docx. Задача 51-storage-c закрыта. Следующая — 51b.


Ротация токена

Токен 5ae781a9... засветился в чате Perplexity Computer при генерации 14.08.2026. Осталось ротировать:

  1. Сгенерировать новый: openssl rand -hex 32 (или в браузере)
  2. Обновить /opt/alatyr-storage-api/.env на ragserver, docker compose restart
  3. Обновить env alatyr-service, restart сервиса
  4. Обновить запись в Vaultwarden
  5. Прибить открытые сессии (все клиенты используют один токен, restart всё разорвёт)

Changelog

  • 11.08.2026 — первый черновик runbook, VPS-first схема
  • 14.08.2026 — код сервиса написан, смоук 15/15, репо создан приватным
  • 14.08.2026 (вечер) — деплой Шагов 1 и 2 выполнен, схема переработана: убран VPS-nginx, DNS указывает напрямую на роутер, TLS на ragserver. Шаг 3 (env + prod E2E) остался.
  • 14.08.2026 (поздний вечер) — Шаг 3 закрыт. PR #11 merged, env на VPS прописан, первый E2E упал в fetch failed из-за Google DNS 8.8.8.8 на VPS со старой A-записью; фикс через resolvectl dns ens1 1.1.1.1 77.88.8.8 (закрепить — прежняя задача 54). Второй E2E прошёл, но вскрылась кракозябра в именах файлов — закрыто PR #23 (5b7c9c4). После повторного E2E с русскими именами — всё корректно. 51-storage-c закрыт. Токен засветился в чате — ротация в CRM-01.