Эксплуатация

Развёртывание, обслуживание и диагностика сервера.

Сервер

Хост см. deploy/.env (в репозиторий не попадает)
Домен api.desperatemeasure.tech
Система Ubuntu 26.04 LTS, Python 3.14.4
Ресурсы 2 ядра, 4 ГБ RAM, 60 ГБ NVMe
Занято 345 МБ проектом, свободно 52 ГБ
Память сервиса около 120 МБ

Доступ по SSH-ключу; аутентификация по паролю отключена.

Адрес сервера в репозитории не хранится — скопируйте deploy/.env.example в deploy/.env и подставьте свой. Команды ниже подразумевают, что переменная экспортирована в текущую оболочку:

set -a && source deploy/.env && set +a
ssh $SERVER

Пароль от root нужен только для веб-консоли в панели хостинга и хранится в менеджере паролей, вне репозитория.

Что где лежит

/srv/carincasa/
  raw/       53 МБ   исходные HTML-страницы и карта категорий
  data/       3 МБ   разобранный каталог
  media/    199 МБ   изображения WebP
  models/      —     3D-модели, наполняется вручную
  venv/      91 МБ   виртуальное окружение
  app/               FastAPI
  scraper/           парсер

/etc/systemd/system/carincasa.service
/etc/caddy/Caddyfile
/var/log/caddy/carincasa.log

Управление сервисом

systemctl status carincasa        # состояние
systemctl restart carincasa       # перезапуск, нужен после пересборки данных
journalctl -u carincasa -f        # логи приложения
journalctl -u carincasa -n 50     # последние 50 строк

Сервис включён в автозапуск и перезапускается сам при падении (Restart=always, пауза 3 секунды).

Uvicorn слушает 127.0.0.1:8000 в два воркера и наружу не смотрит — все запросы проходят через Caddy.

Юнит запущен с ProtectSystem=strict, то есть файловая система для сервиса доступна только на чтение. Это не мешает парсеру: он запускается отдельно, вне юнита.

Caddy

systemctl status caddy
caddy validate --config /etc/caddy/Caddyfile   # проверить конфиг
systemctl reload caddy                          # применить без разрыва соединений
tail -f /var/log/caddy/carincasa.log            # лог доступа в JSON

Сертификат Let's Encrypt Caddy получает и продлевает сам, вмешательства не требуется. Проверить срок:

echo | openssl s_client -connect api.desperatemeasure.tech:443 \
  -servername api.desperatemeasure.tech 2>/dev/null \
  | openssl x509 -noout -dates

Caddy раздаёт /media/* и /models/* прямо с диска, минуя Python. Для USDZ и GLB проставляются MIME-типы model/vnd.usdz+zip и model/gltf-binary — без первого iOS не откроет AR Quick Look.

Добавление 3D-модели

Это единственная операция, которую предполагается делать регулярно. Каталог models/ сейчас пуст, поэтому has_ar у всех товаров равен false.

scp model.usdz $SERVER:/srv/carincasa/models/elio.usdz

Имя файла должно совпадать со slug товара. Перезапускать ничего не нужно: наличие файла проверяется на диске в момент запроса.

Проверка:

curl -s https://api.desperatemeasure.tech/v1/products/elio \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['has_ar'], d['model_url'])"
# True /models/elio.usdz

curl -s -o /dev/null -w "%{http_code} %{content_type}\n" \
  https://api.desperatemeasure.tech/models/elio.usdz
# 200 model/vnd.usdz+zip

Список товаров, у которых модель уже есть:

curl -s "https://api.desperatemeasure.tech/v1/products?has_ar=true" \
  | python3 -c "import json,sys; print([p['slug'] for p in json.load(sys.stdin)])"

Список slug для именования файлов:

curl -s https://api.desperatemeasure.tech/v1/products \
  | python3 -c "import json,sys; [print(p['slug']) for p in json.load(sys.stdin)]"

Поддерживается и .glb, но для AR Quick Look на iOS нужен именно USDZ. Если оба файла лежат рядом, приоритет у USDZ.

Обновление кода

С рабочей машины:

rsync -az scraper/ $SERVER:/srv/carincasa/scraper/
rsync -az app/     $SERVER:/srv/carincasa/app/
rsync -az deploy/carincasa.service $SERVER:/etc/systemd/system/
rsync -az deploy/Caddyfile         $SERVER:/etc/caddy/Caddyfile

ssh $SERVER 'systemctl daemon-reload && systemctl restart carincasa'

После правки Caddyfilesystemctl reload caddy, предварительно проверив конфиг через caddy validate.

Пересборка каталога

Подробности в PARSER.md.

ssh $SERVER
cd /srv/carincasa
./venv/bin/python scraper/parse.py
systemctl restart carincasa

Сайт документации

Документация опубликована на https://desperatemeasure.tech и продублирована на https://api.desperatemeasure.tech/guide/ — второй адрес работает независимо от состояния DNS корневого домена.

Сайт статический и собирается из тех же markdown-файлов, что лежат в репозитории:

rsync -az README.md $SERVER:/srv/carincasa/
rsync -az --exclude specs docs/ $SERVER:/srv/carincasa/docs/
ssh $SERVER 'cd /srv/carincasa && ./venv/bin/python docs/build_site.py'

Перезапускать Caddy не нужно — файлы раздаются с диска.

Порядок страниц и подписи в боковом меню задаются списком PAGES в docs/build_site.py.

Незакрытый хвост в DNS

У корневого домена две A-записи: адрес сервера и 95.163.244.138 (парковка рег.ру). Клиент выбирает из них произвольно, поэтому часть посетителей desperatemeasure.tech попадёт на заглушку регистратора. Вторую запись нужно удалить в панели рег.ру.

Проверка:

dig +short desperatemeasure.tech @8.8.8.8
# должен остаться только $SERVER

Резервное копирование

Восстановимо всё, кроме одного: 3D-модели существуют только на сервере. Каталог и изображения пересобираются из raw/, а raw/ — из сайта, пока он жив.

# то, что действительно нужно беречь
rsync -az $SERVER:/srv/carincasa/models/ ./backup/models/

# полный снимок данных, 255 МБ
rsync -az $SERVER:/srv/carincasa/{raw,data,media} ./backup/

Диагностика

Сервис не поднимается

journalctl -u carincasa -n 40 --no-pager

Чаще всего это ошибка валидации данных при старте: приложение читает data/catalog.json и все карточки через Pydantic, и любое несоответствие схеме роняет запуск. Проверить данные, не трогая сервис:

cd /srv/carincasa && ./venv/bin/python -c "
import sys; sys.path.insert(0, 'scraper')
from models import Product, Catalog
from pathlib import Path
Catalog.model_validate_json(Path('data/catalog.json').read_text())
for f in Path('data/products').glob('*.json'):
    Product.model_validate_json(f.read_text())
print('данные валидны')
"

Отдаётся 502

Значит Caddy жив, а приложение нет.

systemctl status carincasa
curl -s http://127.0.0.1:8000/health     # проверка в обход Caddy

Сертификат не выпускается

Проверьте, что домен резолвится в этот сервер и что 80-й порт открыт — Let's Encrypt проверяет владение через него.

dig +short api.desperatemeasure.tech
ufw status
journalctl -u caddy -n 30 --no-pager | grep -i -E "error|obtain"

Картинка отдаёт 404

Проверьте, что файл на месте — путь в API отражает структуру каталога:

ls /srv/carincasa/media/elio/

Если файлов нет, догрузите по манифесту:

cd /srv/carincasa && ./venv/bin/python scraper/images.py

Повторный запуск пропускает уже готовое и докачивает недостающее.

Файрвол

Открыты 22, 80 и 443. Порт 8000 наружу не смотрит.

ufw status

Проверка после изменений

Быстрый прогон по всему каталогу — все эндпоинты, наличие изображений, целостность цен:

ssh $SERVER "cd /srv/carincasa && ./venv/bin/python - <<'PY'
import httpx
B = 'https://api.desperatemeasure.tech'
with httpx.Client(timeout=40) as c:
    catalog = c.get(B + '/v1/products').json()
    bad = []
    for p in catalog:
        r = c.get(f\"{B}/v1/products/{p['slug']}\")
        if r.status_code != 200:
            bad.append((p['slug'], r.status_code)); continue
        d = r.json()
        if not d['images']:
            bad.append((p['slug'], 'нет изображений'))
    print(f'проверено: {len(catalog)}, проблем: {len(bad)}')
    for b in bad[:10]:
        print('  ', b)
PY"

Ожидаемый результат — проверено: 114, проблем: 0.