MAATRIX / Блог / Grist в Docker Compose: готовый файл

Grist в Docker Compose: готовый файл

MAATRIX

Excel-таблица с формулами до определённого масштаба работает отлично, а потом превращается в файл, который открывается по десять секунд и падает при одновременном редактировании тремя людьми. Airtable решает проблему совместной работы, но лимиты бесплатного тарифа и цена платного быстро становятся неудобными, если данных много. Grist — open-source альтернатива, которая берёт лучшее из обоих миров: таблица с формулами Excel-уровня внутри, но под капотом настоящая SQLite-база с честным SQL и REST API. Разворачивается Grist одним docker-compose.yml, и в этой статье — рабочий файл, который можно поднять на VPS за десять минут.

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

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

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

Что такое Grist и зачем он вам

Grist — это self-hosted инструмент от бывших разработчиков Google Docs, который выглядит как таблица, но ведёт себя как реляционная база. У каждой "таблицы" в Grist есть строгая типизация колонок (текст, число, дата, ссылка на другую таблицу, choice-список), формулы пишутся на Python (не на своём диалекте, как в Excel или Airtable), а данные физически лежат в SQLite — то есть их можно достать SQL-запросом напрямую, если понадобится.

Ключевое отличие от Airtable и NocoDB — формулы. В Grist формула if $Status == "Paid": return $Amount * 0.87 — это валидный Python, с циклами, условиями и доступом к другим таблицам через ссылки. Для тех, кто хоть немного пишет на Python, это ощутимо мощнее, чем зоопарк функций а-ля IF(AND(...)) в обычных таблицах.

Практические сценарии: CRM для небольшой команды без покупки Salesforce, учёт финансов с формулами вместо ручного пересчёта, каталог товаров с связями между таблицами (заказы → клиенты → товары), внутренний трекер задач с автоматическими расчётами дедлайнов. Всё это — без месячной подписки за каждого пользователя, потому что self-hosted версия бесплатна и не ограничивает число редакторов.

Требования к серверу и подготовка

Grist — приложение не самое прожорливое, но и не игрушечное: под капотом Node.js-сервер плюс sandbox для выполнения Python-формул (по умолчанию через gVisor или Pyodide в зависимости от сборки). Для небольшой команды (до 10-15 человек, документы среднего размера) достаточно:

РесурсМинимумКомфортно
CPU1 vCPU2 vCPU
RAM1 GB2 GB
Диск10 GB SSD20+ GB SSD (растёт с документами)
ОСUbuntu 22.04/24.04Ubuntu 24.04

Формулы на больших таблицах (десятки тысяч строк с пересчётом при каждом изменении) заметно грузят CPU — если у вас именно такой сценарий, закладывайте 2 vCPU сразу, не экономьте.

Перед установкой нужен Docker и Docker Compose. На чистом Ubuntu:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker
docker compose version

Если Docker уже стоит и вы просто добавляете ещё один сервис на сервер — общие грабли конфигурации docker-compose для боевого окружения разобраны в статье про частые ошибки production-конфигов.

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

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

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

Готовый docker-compose.yml

Создайте директорию и файл окружения:

mkdir -p /opt/grist/persist
cd /opt/grist
nano .env

Содержимое .env:

GRIST_DOMAIN=grist.example.com
GRIST_ADMIN_EMAIL=admin@example.com
GRIST_SESSION_SECRET=замените-на-случайную-строку-минимум-32-символа

Секрет сессии сгенерируйте командой openssl rand -hex 32 — не оставляйте плейсхолдер.

Сам docker-compose.yml:

version: "3.8"

services:
  grist:
    image: gristlabs/grist:latest
    container_name: grist
    restart: unless-stopped
    ports:
      - "127.0.0.1:8484:8484"
    environment:
      - PORT=8484
      - GRIST_SESSION_SECRET=${GRIST_SESSION_SECRET}
      - GRIST_DEFAULT_EMAIL=${GRIST_ADMIN_EMAIL}
      - GRIST_SINGLE_ORG=true
      - APP_HOME_URL=https://${GRIST_DOMAIN}
      - GRIST_SANDBOX_FLAVOR=gvisor
    volumes:
      - ./persist:/persist
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost:8484"]
      interval: 30s
      timeout: 5s
      retries: 3

Важные моменты по переменным:

  • GRIST_SINGLE_ORG=true — упрощённый режим одной организации без мультитенантности, подходит для команды или личного использования. Если планируете хостить несколько независимых организаций на одном Grist — уберите эту переменную и читайте документацию по multi-org режиму отдельно.
  • GRIST_SANDBOX_FLAVOR=gvisor — движок выполнения формул. На части VPS-провайдеров gVisor не заводится из-за отсутствия нужных возможностей ядра в контейнере (часто встречается на OpenVZ-виртуализации, реже на KVM). Если контейнer падает при попытке выполнить формулу с ошибкой sandbox — смените на unsandboxed (менее безопасно, но работает везде) или pyodide (WebAssembly, медленнее, но переносимо).
  • 127.0.0.1:8484:8484 — сервис слушает только localhost, наружу отдаётся через reverse proxy с HTTPS (ниже).

Порт 8484 — стандартный порт Grist в докер-образе, менять его без причины не нужно.

HTTPS и reverse proxy

Отдавать Grist напрямую по HTTP на публичный порт — плохая идея: сессионные куки и API-токены должны ходить только по TLS. Проще всего поднять Caddy — он сам получает сертификат Let's Encrypt и обновляет его без вашего участия.

Если Caddy ещё не установлен, шаги разобраны в статье про установку и частые ошибки Caddy с авто-SSL. Конфиг для Grist — /etc/caddy/Caddyfile:

grist.example.com {
    reverse_proxy 127.0.0.1:8484
    encode gzip

    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
    }
}

Перезапуск:

sudo systemctl reload caddy

Если предпочитаете Traefik с автоматическим обнаружением контейнеров через label — соответствующие label вместо секции ports можно добавить прямо в docker-compose.yml (traefik.enable=true, traefik.http.routers.grist.rule=Host(...) и так далее); базовый пример такой связки — в статье про Traefik как reverse proxy для Docker.

Запуск самого Grist:

docker compose up -d
docker compose logs -f grist

После первого старта откройте https://grist.example.com/ — вас встретит форма создания первого пользователя (email из GRIST_DEFAULT_EMAIL уже будет администратором организации).

Бэкапы: что и как сохранять

Все данные Grist — документы, метаданные пользователей, настройки — лежат в volume ./persist. Никакой отдельной СУБД снаружи нет: каждый документ Grist физически хранится как отдельный SQLite-файл внутри persist/docs/.

Простой ежедневный бэкап через cron:

mkdir -p /opt/grist-backups
cat > /opt/grist/backup.sh << 'EOF'
#!/bin/bash
DATE=$(date +%Y-%m-%d_%H%M)
BACKUP_DIR=/opt/grist-backups
docker compose -f /opt/grist/docker-compose.yml stop grist
tar czf "$BACKUP_DIR/grist_$DATE.tar.gz" -C /opt/grist persist
docker compose -f /opt/grist/docker-compose.yml start grist
find "$BACKUP_DIR" -name "grist_*.tar.gz" -mtime +14 -delete
EOF
chmod +x /opt/grist/backup.sh

Останавливать контейнер перед архивированием — не строго обязательно (SQLite умеет писать консистентно и без этого через WAL), но так вы гарантированно избегаете чтения файла в момент активной записи. Для команды, которая работает в Grist не круглосуточно, минутный простой ночью незаметен.

Добавьте в cron:

crontab -e
# 0 3 * * * /opt/grist/backup.sh >> /opt/grist/backup.log 2>&1

Для более серьёзной схемы — с версионированием, инкрементальными снимками и хранением вне сервера — стоит присмотреться к готовым решениям вроде BorgBackup или Duplicati, которые умеют работать поверх любых каталогов, включая persist.

Дополнительно у Grist есть встроенный экспорт документа целиком (меню документа → Export → .grist файл) — удобно для разового снимка перед рискованным изменением структуры таблицы, не заменяет системный бэкап.

API и импорт данных

У Grist полноценный REST API — токен генерируется в профиле пользователя (Profile Settings → API). Базовый запрос для чтения записей таблицы:

curl -H "Authorization: Bearer ВАШ_ТОКЕН" \
  "https://grist.example.com/api/docs/DOC_ID/tables/TABLE_NAME/records"

Запись новой строки:

curl -X POST \
  -H "Authorization: Bearer ВАШ_ТОКЕН" \
  -H "Content-Type: application/json" \
  -d '{"records": [{"fields": {"Name": "Новая запись", "Amount": 1500}}]}' \
  "https://grist.example.com/api/docs/DOC_ID/tables/TABLE_NAME/records"

DOC_ID берётся из URL открытого документа — это буквенно-цифровая строка после /doc/. API удобен для интеграций: например, вебхук из формы на сайте пишет заявки прямо в таблицу Grist, а формулы на Python уже считают статусы и суммы.

Импорт существующих данных: Grist умеет читать .csv, .xlsx и даже подключаться напрямую к Google Sheets при импорте (кнопка Import в интерфейсе). Формулы и типы колонок при импорте из Excel Grist пытается угадать по данным — их всё равно стоит проверить руками после переноса, автоопределение не всегда точное.

Grist vs Airtable vs NocoDB — когда что выбрать

Если рассматриваете self-hosted вариант вообще, логично сравнить Grist с соседями по нише:

GristNocoDBAirtable (SaaS)
ФормулыPython, полноценныйJS-подобные, ограниченныеСобственный DSL
ХранилищеSQLite на документPostgreSQL/MySQLПроприетарное
Self-hostedДа, бесплатноДа, бесплатноНет
APIREST, богатыйRESTREST
Порог входаСредний (Python)НизкийНизкий
Лучше дляРасчётные таблицы, финансыБазы данных как UI над SQLБыстрый старт без сервера

NocoDB выигрывает, если вам нужна таблица как визуальный слой поверх уже существующей PostgreSQL/MySQL базы — тогда данные остаются в вашей БД, а NocoDB просто рисует интерфейс. Если такой сценарий ближе — смотрите статью NocoDB в Docker Compose и разбор NocoDB или Baserow — что выгоднее, где логика выбора между self-hosted таблично-базовыми инструментами разобрана подробнее.

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

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

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

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

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

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

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

Grist работает без интернета, полностью офлайн?

Веб-интерфейс требует запущенного сервера (локального или на VPS), но сам сервер не обращается наружу для работы — лицензионных проверок или телеметрии по умолчанию в open-source образе нет. Есть и десктопное Electron-приложение для полностью локальной работы без сервера вообще.

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

Да, двумя способами: скопировать файл .grist конкретного документа (в persist/docs/) на новый сервер с тем же образом Grist, либо экспортировать документ через интерфейс и импортировать на другой инсталляции.

Формулы на Python — это безопасно, если документ шарят внешним пользователям?

Формулы выполняются в sandbox (gVisor или Pyodide), изолированном от хост-системы, но всё равно ограничивайте доступ к серверу редактирования формул только доверенным пользователям — sandbox снижает риск, а не убирает его полностью.

Сколько документов и пользователей выдержит минимальная конфигурация в 1 vCPU / 1 GB?

Для команды до 5-7 человек с несколькими документами среднего размера (пара тысяч строк) должно хватить с запасом; конкретная цифра зависит от сложности формул и частоты одновременных правок, ориентируйтесь на мониторинг нагрузки в первую неделю и добавляйте ресурсы по факту.

Нужна ли отдельная база данных PostgreSQL для Grist?

Нет, в стандартной поставке метаданные организации тоже хранятся в SQLite внутри persist. Внешний PostgreSQL — опция для крупных enterprise-инсталляций Grist, для обычного self-hosted сценария не требуется.

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

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

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