Эксплуатация
Развёртывание, обслуживание и диагностика сервера.
Сервер
| Хост | см. 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'
После правки Caddyfile — systemctl 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.