# DomCrawler на Linux/VPS

Парсер обычно работает как CLI-задача импорта, а не как отдельный веб-сервис. Храните код/вход/отчёты вне публичной директории. Пример Ubuntu/Debian после установки PHP8.2+ CLI, XML и mbstring:

```bash
sudo useradd --system --home /opt/domcrawler --shell /usr/sbin/nologin domcrawler
sudo install -d -o domcrawler -g domcrawler -m 0750 /opt/domcrawler
sudo install -d -o domcrawler -g domcrawler -m 0750 /var/lib/domcrawler
# Скопируйте распакованный проект в /opt/domcrawler, затем:
sudo chown -R domcrawler:domcrawler /opt/domcrawler
cd /opt/domcrawler
sudo -u domcrawler composer install --no-dev --no-interaction --no-plugins --prefer-dist
sudo -u domcrawler php bin/extract.php --input fixtures/cards.html --output /var/lib/domcrawler/services.json
```

Сначала тестируйте вымышленную фикстуру. `--output` заменяет целевой JSON, поэтому не указывайте важный файл. При коде2 не считайте частичный/пустой результат готовой новой базой. Само извлечение идемпотентно для одинакового входа, а запись в приложение должна иметь собственную стратегию обновления, транзакцию и проверку количества строк.

## Запуск по расписанию

/etc/systemd/system/domcrawler-import.service:

```ini
[Unit]
Description=Extract prepared HTML into a review report

[Service]
Type=oneshot
User=domcrawler
Group=domcrawler
WorkingDirectory=/opt/domcrawler
ExecStart=/usr/bin/php bin/extract.php --input /var/lib/domcrawler/input.html --output /var/lib/domcrawler/services.json
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/lib/domcrawler
TimeoutStartSec=60
MemoryMax=256M
UMask=0027
```

Путь `/usr/bin/php` сверяется командой `command -v php`. Значения времени/памяти — начальные пределы примера, корректируются по измерениям реального файла. Служба не загружает вход из сети: отдельный контролируемый этап должен положить UTF-8 HTML в input.html. Это делает источник, права и сетевую политику явными.

/etc/systemd/system/domcrawler-import.timer:

```ini
[Unit]
Description=Run prepared HTML extraction daily
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
```

```bash
sudo systemctl daemon-reload
sudo systemctl start domcrawler-import.service
sudo systemctl status domcrawler-import.service
sudo journalctl -u domcrawler-import.service -n 50 --no-pager
# Только после проверки входа и результата:
sudo systemctl enable --now domcrawler-import.timer
systemctl list-timers domcrawler-import.timer
# Остановка расписания:
sudo systemctl disable --now domcrawler-import.timer
```

Коды1 и2 приводят к failed unit и требуют разбора. Один timer не запускает уже активный экземпляр той же service, но ручной запуск PHP или другой service может идти параллельно. Координация нескольких путей запуска — отдельная задача, например Symfony Lock. Здесь сетевой HTTP-клиент, cron и реальная БД намеренно отсутствуют.

## Публичная мастерская

На сайте достаточно готового каталога public. Если публикуете её отдельно, скопируйте только public в документ-root Nginx/Caddy; vendor, входные документы и рабочие JSON клиентов туда не переносите. ZIP лаборатории содержит только вымышленные фикстуры. PHP в публичной мастерской отсутствует: браузер читает сохранённую запись.

## Сетевой этап отдельно

Если добавляете Guzzle/HttpClient: используйте разрешённые источники, таймаут соединения/чтения, ограничение размера, проверку HTTP-статуса/Content-Type, контролируемые редиректы и политику доступа к внутренним адресам. Проверка строки host сама по себе не защищает от DNS rebinding и SSRF. Не стройте открытый endpoint «скачать любой URL». Не записывайте cookies/токены/весь приватный HTML в обычный журнал.
