MAATRIX / Блог / Grist на сервере: частые ошибки и решения

Grist на сервере: частые ошибки и решения

MAATRIX

Grist — это гибрид таблицы и базы данных: интерфейс похож на Excel или Google Sheets, но под капотом реляционная модель, формулы на Python и полноценный REST API. На бумаге self-hosted версия разворачивается одной командой Docker. На практике на собственном VPS вылезают нюансы, которых нет в кратком README: песочница для формул отказывается стартовать на части хостингов, документы хранятся не в одной базе, а десятками отдельных SQLite-файлов, вебхуки молча блокируются защитой от SSRF, а неудачно настроенные правила доступа запросто закрывают вход самому владельцу. Разберём это по порядку — с конкретными командами и конфигами.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Установка Grist через Docker Compose

Официальный образ gristlabs/grist разворачивается быстро, но большинство проблем в статье связаны именно с тем, что при первом старте не задали ключевые переменные окружения.

services:
  grist:
    image: gristlabs/grist:latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:8484:8484"
    environment:
      PORT: 8484
      APP_HOME_URL: https://grist.example.com
      GRIST_SESSION_SECRET: замените_на_случайную_строку_32+_символа
      GRIST_SANDBOX_FLAVOR: gvisor
      GRIST_DEFAULT_EMAIL: admin@example.com
      ALLOWED_WEBHOOK_DOMAINS: example.com,hooks.example.org
    volumes:
      - ./persist:/persist

Три вещи, которые часто пропускают. Во-первых, APP_HOME_URL должен точно совпадать с адресом, по которому Grist будет доступен снаружи, — иначе куки сессии выставляются не на тот домен, и вход в интерфейс зацикливается на странице логина. Во-вторых, GRIST_SESSION_SECRET нужно задать явно и зафиксировать: если оставить дефолт или менять его при каждом пересоздании контейнера, все пользователи разлогиниваются при каждом рестарте. В-третьих, том /persist — единственное место, где живут все ваши документы и настройки; без него любой пересбор контейнера обнуляет данные.

Для небольшой команды SQLite «из коробки» достаточно. Для инсталляции с десятками пользователей и активной работой через API Grist умеет выносить служебную базу метаданных (пользователи, организации, воркспейсы) во внешний PostgreSQL через переменные TYPEORM_* — это отдельная настройка, и на старте с ней лучше не усложнять.

Песочница для формул не стартует

Формулы в Grist пишутся на Python и выполняются не «как есть», а в изолированной песочнице — по умолчанию через gVisor (GRIST_SANDBOX_FLAVOR: gvisor). Это самая частая причина, по которой self-hosted Grist вообще не поднимается на некоторых VPS: gVisor эмулирует часть системных вызовов ядра, и на урезанных или контейнеризированных окружениях (некоторые OpenVZ/LXC-тарифы, Docker без нужных capabilities, ограниченный seccomp-профиль у хостера) он падает с ошибками вида sandbox exec error или формулы в таблицах просто перестают пересчитываться, оставляя #ERROR в ячейках.

Проверяется логами контейнера:

docker compose logs grist --tail 100 | grep -i sandbox

Если там ошибки запуска песочницы — на VPS с полноценным KVM-ядром (типичная аренда выделенного сервера или VPS с честной виртуализацией) проблема обычно решается сама после перезапуска с чистого образа. Если хостинг действительно режет нужные системные вызовы, есть два обходных пути:

environment:
  GRIST_SANDBOX_FLAVOR: unsandboxed  # формулы выполняются без изоляции

или более безопасный компромисс — pyodide (формулы выполняются в WebAssembly-рантайме, без прямого доступа к системным вызовам хоста, но чуть медленнее gVisor). Режим unsandboxed стоит использовать только если вы полностью доверяете всем, кто пишет формулы в документах, — при unsandboxed вредоносная формула технически может обратиться к файловой системе контейнера.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Потеря данных и стратегия бэкапов

Ключевая особенность архитектуры Grist, которая ломает интуицию людей, привыкших к «одна база — один дамп»: каждый документ — это отдельный файл SQLite в /persist/docs/, плюс общая служебная база /persist/home.sqlite3 с метаданными пользователей и прав. Бэкап «на глаз» одного файла не спасает — при восстановлении легко получить рассинхрон между документами и метаданными об организациях и правах доступа к ним.

Правило: бэкапить весь каталог /persist целиком, одним снимком.

# горячий бэкап (для SQLite это в целом безопасно, но лучше делать в период низкой активности)
tar -czf grist-backup_$(date +%F).tar.gz ./persist

# более аккуратный вариант — с кратковременной остановкой контейнера,
# чтобы гарантированно не поймать документ в середине записи
docker compose stop grist
tar -czf grist-backup_$(date +%F).tar.gz ./persist
docker compose start grist

Для регулярных автоматических бэкапов такую команду стоит завернуть в cron и хранить хотя бы 7-14 последних снимков — как это устроено в целом для бэкапа Docker-томов на сервере, Grist здесь не исключение из общих правил.

Отдельная страховка на случай повреждения самого файла SQLite документа — экспорт через API или интерфейс (меню документа → Export → CSV/Grist-формат) для критичных таблиц раз в день или неделю. Это не заменяет полный бэкап, но снимок в человекочитаемом формате помогает восстановить данные, если основной файл документа окажется битым, а бэкап каталога — устаревшим.

Reverse proxy, HTTPS и WebSocket для совместного редактирования

Как и большинство современных таблично-табличных интерфейсов, Grist использует WebSocket для realtime-обновлений: когда два человека одновременно открывают документ, изменения одного должны появляться у другого без перезагрузки страницы. Если прокси не пробрасывает Upgrade-заголовки, интерфейс открывается и работает, но совместное редактирование в реальном времени тихо ломается — обновления приходят только после ручного обновления страницы.

Рабочий конфиг nginx:

server {
    listen 443 ssl http2;
    server_name grist.example.com;

    ssl_certificate     /etc/letsencrypt/live/grist.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/grist.example.com/privkey.pem;

    client_max_body_size 100m;  # импорт больших CSV/XLSX иначе упрётся в 413

    location / {
        proxy_pass http://127.0.0.1:8484;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_header;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 3600s;
    }
}

Если вместо nginx используете Traefik как reverse proxy для Docker, проброс WebSocket настраивать отдельно не нужно — он проксирует апгрейд соединения автоматически по меткам сервиса, что заметно снижает число подобных проблем на старте.

Отдельно проверьте, что APP_HOME_URL в переменных окружения контейнера использует https://, а не http:// — если схема не совпадает с реальным протоколом, за которым Grist работает через прокси, часть внутренних ссылок и редиректов в интерфейсе будет вести на неработающий адрес.

API, вебхуки и интеграции: типичные ошибки

Grist даёт полноценный REST API поверх каждого документа — можно читать и писать записи без интерфейса:

curl -H "Authorization: Bearer ВАШ_API_KEY" \
  "https://grist.example.com/api/docs/DOC_ID/tables/Таблица/records"

API-ключ выпускается в настройках профиля пользователя, а не документа, и наследует все права этого пользователя — если ключ утечёт, у злоумышленника окажется доступ ко всем документам, на которые есть права у владельца ключа. Держите ключи от имени сервисных, а не личных аккаунтов там, где это возможно.

С исходящими вебхуками (уведомление внешнего URL при изменении записи) частая жалоба — вебхук настроен в интерфейсе, но события не долетают. Причина обычно в переменной ALLOWED_WEBHOOK_DOMAINS: это защита от SSRF, и по умолчанию Grist не разрешает произвольные исходящие запросы с сервера. Домен назначения нужно явно перечислить в переменной окружения контейнера:

environment:
  ALLOWED_WEBHOOK_DOMAINS: hooks.example.com,api.partner-service.com

Без перезапуска контейнера после изменения этой переменной вебхуки продолжат молча отваливаться — переменные окружения читаются при старте процесса, docker compose up -d --force-recreate grist обязателен после правки.

Второй нюанс — вебхуки в Grist не гарантируют мгновенную доставку и ретраи «из коробки» так же настраиваемо, как в специализированных очередях сообщений: при недоступности принимающего сервера событие может быть потеряно, а не поставлено в очередь на бесконечные повторные попытки. Для критичных интеграций надёжнее держать на принимающей стороне идемпотентный обработчик и периодически сверять состояние через API, а не полагаться только на вебхуки как единственный источник истины.

Права доступа (ACL) и многопользовательская работа

У Grist двухуровневая модель прав: роли на уровне организации/воркспейса (владелец, редактор, наблюдатель) и отдельные Access Rules внутри конкретного документа — гибкие правила на формулах Python, которые могут скрывать отдельные строки или столбцы для определённых пользователей.

Самая частая ошибка новичков — настроить Access Rules так, что они случайно перекрывают доступ самому владельцу документа. Формулы правил выполняются по порядку сверху вниз, и если общее ограничивающее правило стоит выше исключения для владельца, система применит именно ограничивающее — владелец увидит пустую таблицу или получит отказ в доступе к собственному документу. Восстановить доступ можно, зайдя под этим же аккаунтом через раздел управления документом в веб-интерфейсе организации (там правила ACL можно отключить извне, не открывая сам документ) — либо, в крайнем случае, напрямую отредактировав служебные таблицы правил через API от имени администратора инстанса.

Второй частый вопрос — можно ли дать доступ «только на чтение» ко всей организации, но с правом редактирования у конкретного документа. Да, это стандартный сценарий: роль Viewer на уровне воркспейса плюс отдельно выданная роль Editor на конкретный документ — Grist поддерживает переопределение прав ниже по иерархии, документ не обязан наследовать ровно ту же роль, что и родительский воркспейс.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Grist точно можно развернуть без Docker, напрямую как Node.js-приложение?

Да, есть вариант установки из исходников через yarn, но тогда песочница для формул, автозапуск и обновления придётся настраивать вручную — для сервера без постоянного присмотра Docker Compose проще поддерживать в рабочем состоянии.

Почему после смены GRIST_SESSION_SECRET все пользователи разлогинились?

Это ожидаемое поведение — секрет используется для подписи cookie сессии, и при его смене все существующие сессии становятся невалидными. Меняйте секрет только осознанно, не при каждом пересоздании контейнера.

Сколько оперативной памяти нужно под Grist на небольшую команду?

Для 5-10 пользователей с несколькими документами обычно достаточно 1-2 ГБ, но это ориентир, а не гарантия — реальное потребление сильно зависит от размера документов и сложности формул, точные цифры лучше снять на своей нагрузке через docker stats.

Можно ли мигрировать документ Grist на другой сервер?

Да, документ — это один файл SQLite из /persist/docs/ с расширением .grist, его можно скопировать и импортировать на другой инстанс через интерфейс или просто положить в /persist/docs/ целевого сервера с последующей регистрацией в служебной базе.

Что делать, если формулы работают на одном документе, но выдают #ERROR на другом после переноса?

Проверьте, что режим песочницы (GRIST_SANDBOX_FLAVOR) одинаковый на обоих серверах — некоторые конструкции Python могут вести себя по-разному между gvisor, unsandboxed и pyodide, особенно если формулы обращаются к внешним библиотекам.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →