Deployment guide

Self-Hosting Grist on a VPS: Open-Source Airtable Alternative

Deploy on a VPS Cloud →

Tutorial

Self-Hosting Grist on a VPS: Open-Source Airtable Alternative

Self-hosting7 min read3 steps

Google Sheets stores lists. Airtable adds relations between tables but charges $20 per user per month. Grist does both, open-source, on your own server: typed columns, references between tables, Python formulas running server-side, and a REST API generated automatically on every document. The result is a complete data modelling tool — no subscription, no vendor lock-in.

Contents· Why Grist instead of Google Sheets or Airtable1/9
  1. 01Why Grist instead of Google Sheets or Airtable
  2. 02What Grist does that Google Sheets cannot
  3. 03Requirements and Docker architecture
  4. 04Full Docker Compose and deployment
  5. 05Advanced Grist formulas: VLOOKUP and cross-table references
  6. 06SSO authentication with OIDC (optional)
  7. 07Backups: Docker volumes and export
  8. 08Troubleshooting
  9. 09Grist vs NocoDB vs Baserow: choosing your open-source spreadsheet

Why Grist instead of Google Sheets or Airtable

Google Sheets is a spreadsheet: it stores data flat, calculations are limited to sheet functions, and relations between sheets rely on fragile VLOOKUPs. Airtable solves this with relational bases but at $20 per user per month, the bill adds up fast for an agency or technical team.

Grist combines both worlds: a familiar spreadsheet interface over a real relational database. Each document contains tables with typed columns (text, numeric, date, reference, attachment), explicit relations between tables via reference columns, and Python formulas that run server-side — not in the browser. On your VPS, Grist costs $0 extra per month.

What Grist does that Google Sheets cannot

  • Relations between tables: a reference column in Orders points to Customers — not a breakable VLOOKUP, a real foreign key with autocomplete.
  • Server-side Python formulas: import datetime, import re, import uuid — and any pure-Python library. Formulas execute in a server sandbox, not the browser.
  • Automatic REST API: every document exposes /api/docs/<id>/tables/<table>/records without configuration. Integrate Grist with n8n, Make or any HTTP client.
  • Multiple views on the same data: grid, card, chart, calendar, custom widget — each view is filtered and configured independently without duplicating data.
  • Per-row, per-column access rules: restrict read or write access to a row based on user attributes — granular enough for multi-tenant documents.

Requirements and Docker architecture

A VPS with at least 1 GB of RAM and Docker installed is sufficient to get started. Grist deploys as a single gristlabs/grist container bundling Node.js (web server), Python (formula sandbox) and SQLite (document storage). A named volume /persist stores .grist files — one file per document. No external service required: no PostgreSQL, no Redis, no Celery.

The default port is 8484. Verify it is reachable from localhost before configuring the reverse proxy (curl -sI http://localhost:8484/).

Full Docker Compose and deployment

  1. Create the directory and docker-compose.yml

    SSH into your VPS and create the stack directory:

    mkdir -p /opt/stacks/grist && cd /opt/stacks/grist

    Create the following docker-compose.yml:

    services:
      grist:
        image: gristlabs/grist:1.7.20
        container_name: grist
        restart: unless-stopped
        ports:
          - "127.0.0.1:8484:8484"
        volumes:
          - grist_data:/persist
        environment:
          GRIST_SESSION_SECRET: "REPLACE_WITH_openssl_rand_hex_32"
          APP_HOME_URL: "https://grist.yourdomain.com"
          GRIST_DEFAULT_EMAIL: "[email protected]"
          GRIST_SANDBOX_FLAVOR: "gvisor"
    
    volumes:
      grist_data:

    Generate the session secret before starting: openssl rand -hex 32. Never reuse this value across instances.

  2. Start and verify

    docker compose up -d
    docker compose logs -f grist

    Wait for the Grist server ready message. Check the HTTP status:

    curl -sI http://localhost:8484/

    You should get HTTP/1.1 200 OK or a 302 redirect. If the container keeps restarting, see the Troubleshooting section below.

  3. Configure nginx as HTTPS reverse proxy

    Install nginx and certbot on the VPS, then create /etc/nginx/sites-available/grist.conf:

    server {
        listen 80;
        server_name grist.yourdomain.com;
        return 301 https://$host$request_uri;
    }
    
    server {
        listen 443 ssl;
        server_name grist.yourdomain.com;
    
        ssl_certificate /etc/letsencrypt/live/grist.yourdomain.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/grist.yourdomain.com/privkey.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
    
        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
        add_header X-Frame-Options SAMEORIGIN always;
        add_header X-Content-Type-Options nosniff always;
        add_header Referrer-Policy strict-origin-when-cross-origin always;
    
        location / {
            proxy_pass http://127.0.0.1:8484;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_read_timeout 300s;
        }
    }

    Obtain the certificate and enable the vhost:

    certbot certonly --nginx -d grist.yourdomain.com
    ln -s /etc/nginx/sites-available/grist.conf /etc/nginx/sites-enabled/
    nginx -t && systemctl reload nginx

    Then update APP_HOME_URL in the Compose file and restart: docker compose up -d.

Advanced Grist formulas: VLOOKUP and cross-table references

Grist replaces fragile Google Sheets VLOOKUPs with reference columns and typed lookup functions. In an Orders table with a Customer column of type Reference → Customers, accessing the customer name is simply $Customer.Name — no VLOOKUP needed.

For aggregations, Grist provides lookupRecords() and lookupOne():

# In the Customers table — total order amount for a customer
SUM(Orders.lookupRecords(Customer=$id).Amount)

# In the Orders table — retrieve the customer's postal code
Customers.lookupOne(Name=$Customer.Name).PostalCode

# Progressive reduction using standard Python
import datetime
delay = (datetime.date.today() - $OrderDate).days
$Amount * (1 - 0.02 * delay // 30)

Python formulas run inside a gVisor sandbox (GRIST_SANDBOX_FLAVOR=gvisor): the Python process has no access to the filesystem or network, isolating computations even in multi-tenant contexts.

SSO authentication with OIDC (optional)

By default, GRIST_DEFAULT_EMAIL automatically logs in a single user without a password — sufficient for solo use or a trusted team on a private network. For multi-user deployments with real authentication, Grist supports OpenID Connect (OIDC) with Authentik, Keycloak, Google Workspace or any compatible provider.

Add these variables to the Compose file:

environment:
  GRIST_OIDC_IDP_ISSUER: "https://auth.yourdomain.com/application/o/grist/"
  GRIST_OIDC_IDP_CLIENT_ID: "your-client-id"
  GRIST_OIDC_IDP_CLIENT_SECRET: "your-client-secret"
  GRIST_OIDC_IDP_SCOPES: "openid email profile"
  GRIST_OIDC_SP_HOST: "https://grist.yourdomain.com"

Remove GRIST_DEFAULT_EMAIL once OIDC is configured — the two variables are incompatible. The first user to log in via OIDC becomes the site administrator.

Backups: Docker volumes and export

Grist documents live in the Docker volume grist_data, under /persist/docs/. Two complementary strategies:

Volume snapshot (full backup, recommended): mount /persist/docs/ in a Backrest job (available in the ServOrbit catalogue). Backrest snapshots the folder via restic and pushes encrypted increments to S3, Backblaze B2 or SFTP — without stopping Grist.

# Emergency manual backup
docker run --rm \
  -v grist_data:/source:ro \
  -v /backups/grist:/dest \
  alpine tar czf /dest/grist-$(date +%Y%m%d).tar.gz -C /source .

Per-document export: in Grist, go to Document → Export → .grist (native SQLite format) or CSV/XLSX per table. The .grist export is sufficient to migrate a single document to another instance.

Grist also keeps a per-document snapshot history (Document → History) — useful for restoring a previous version without leaving the interface, up to the configured retention limit.

Troubleshooting

Container keeps restarting: check the logs (docker compose logs grist). The most common cause is an empty or too-short GRIST_SESSION_SECRET — it must be at least 32 hexadecimal characters. Also check volume permissions: docker exec grist ls -la /persist.

CORS errors on the API: if you call /api/docs/… from a different domain, add GRIST_ALLOWED_HOSTS=grist.yourdomain.com and make sure APP_HOME_URL matches the public URL exactly (with https://, no trailing slash).

Slowness on large datasets: Grist re-evaluates all formulas on every change. On tables with more than 100,000 rows, limit lookupRecords() to indexed columns and avoid Python formulas with heavy imports in trigger columns. The GRIST_MAX_ROWS_PER_TABLE variable (unbounded by default) allows setting a preventive limit.

Access issues after domain change: update APP_HOME_URL, clear browser cookies and restart the container. Active sessions use the old URL and become invalid.

Grist vs NocoDB vs Baserow: choosing your open-source spreadsheet

Scroll the table

CriterionGristNocoDBBaserow
StorageSQLite per document (.grist file)PostgreSQL or MySQL (shared DB)PostgreSQL (shared DB)
FormulasServer-side Python (sandbox)No native formulasLimited formulas, client-side JS
REST APIAutomatic per documentAutomatic per baseAutomatic
SSO authNative OIDCLDAP, OIDC (Enterprise)SSO paid (plans > Pro)
BackupsOne .grist file per documentPostgreSQL dumpPostgreSQL dump
Ideal use caseData modelling, complex formulasTeam-oriented Airtable replacementNo-code interface for business teams
Resources (startup)1 GB RAM, Docker2 GB RAM, Docker + PostgreSQL2 GB RAM, Docker + PostgreSQL

To update Grist, change the image tag in the Compose file (gristlabs/grist:1.7.20 → next version), stop the container (docker compose down), then restart (docker compose up -d). The grist_data volume persists across versions. Check the release notes at github.com/gristlabs/grist-core before each update — some versions introduce schema migrations that are not reversible.

Your Airtable, on your own server

Deploy Grist on a ServOrbit VPS and model your data without a subscription, without vendor lock-in — REST API, Python formulas, real-time collaboration.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab