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/tcp → 192.168.1.200. Storage-api идёт по тому же паттерну что rag.alatyr-service.ru — VPS в цепочке не участвует:
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¶
Пропагация ~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¶
- Открыть карточку любого объекта в admin-панели
- Загрузить тестовый файл (небольшой PDF или txt)
- Проверить что файл появился в
/opt/alatyr-storage/objects/<slug>/на ragserver - Скачать через UI — контент совпадает
- Удалить — файл исчезает с диска
Если 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. Осталось ротировать:
- Сгенерировать новый:
openssl rand -hex 32(или в браузере) - Обновить
/opt/alatyr-storage-api/.envна ragserver,docker compose restart - Обновить env alatyr-service, restart сервиса
- Обновить запись в Vaultwarden
- Прибить открытые сессии (все клиенты используют один токен, 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.