---
title: "Deploy Grist | (Just Updated) Self-Hosted Spreadsheet Database With Python Formulas and API, Locked With a Generated Password, Data Survives Redeploys"
description: "Grist self-hosted spreadsheet database, Python formulas, REST API, locked"
category: "Other"
url: https://railway.com/deploy/grist-or-just-updated-self-hosted-spread
---

# Deploy Grist | (Just Updated) Self-Hosted Spreadsheet Database With Python Formulas and API, Locked With a Generated Password, Data Survives Redeploys

Grist self-hosted spreadsheet database, Python formulas, REST API, locked

**[Deploy Grist | (Just Updated) Self-Hosted Spreadsheet Database With Python Formulas and API, Locked With a Generated Password, Data Survives Redeploys on Railway](https://railway.com/template/grist-or-just-updated-self-hosted-spread)**

Machine-readable deploy manifest (JSON, validated by TemplateCI): https://railway.com/deploy/grist-or-just-updated-self-hosted-spread/manifest.json

- **Creator:** SuperSlowSloth
- **Category:** Other

## Template content

### grist

- **Image:** ghcr.io/bon5co/grist-railway:1.7.22@sha256:93a11b2097aad9fdf547e51fd5d0ac2c95b1686520014795d2c28981060181df
- **Health check:** /health
- **Public domain:** Yes

## Documentation

# Deploy and Host Grist on Railway

**Built for an LLM agent to deploy and use.** The deploy form has no fields to fill: the whole app sits behind a generated password, Railway's port is bound, the Python formula sandbox is enabled and the data volume is mounted. Grist is an open-source relational spreadsheet: tables with typed columns, linked records, Python formulas and a REST API, so an agent can create a document, add a table and read computed values back with plain HTTP requests.

## About Hosting Grist

This template runs one service, **grist** (the web app and its REST API on one public domain) with a volume at `/persist` that holds the document files and the home database, so a redeploy loses nothing.

**Locked down from boot.** grist-core has no password login: a stock container either needs an external identity provider or, in its default mode, makes everyone who reaches the site the owner. This template puts an nginx gateway in front of Grist: every path except `/health` requires HTTP Basic auth with user `admin` and the password in `GRIST_PASSWORD`, a 32-character value generated per deploy, and the gateway tells Grist who the caller is with a header it overwrites on every request, so a caller cannot choose an identity. The wrapper refuses to start without a password of at least 16 alphanumeric characters. Grist itself listens on loopback only. Formulas run in Grist's pyodide sandbox, because the default gVisor sandbox needs privileges Railway does not grant. Upstream is Grist v1.7.22.

**Using it.** Open `https://YOUR_DOMAIN`, sign in as `admin` with `GRIST_PASSWORD` (a browser prompts for it), and create documents. Hand the password only to people who should have access.

### Agent contract

- Public URL: `https://RAILWAY_PUBLIC_DOMAIN` (the generated domain of the `grist` service; the REST API is under `/api`)
- Private URL: `http://grist.railway.internal:8080`
- Credentials: the variable `GRIST_PASSWORD` on the `grist` service; the user name is always `admin`
- Auth: HTTP Basic, `curl -u "admin:$GRIST_PASSWORD"`. Without credentials, or with a wrong password, every path except `/health` answers `401`. `GET /api/orgs` lists the site (use its `id`), `POST /api/orgs/ID/workspaces` creates a workspace, `POST /api/workspaces/ID/docs` a document, `POST /api/docs/ID/tables` and `/api/docs/ID/tables/TABLE/records` fill it.
- Health: `GET /health` returns 200 (no credentials needed)

```bash
#!/bin/sh
# needs: RAILWAY_PUBLIC_DOMAIN, GRIST_PASSWORD (variables of the grist service); also curl. Creates one workspace named "agent-check"
# with one document in it; delete it from the UI or with DELETE /api/workspaces/ID afterwards.
B="https://$RAILWAY_PUBLIC_DOMAIN"
A="admin:$GRIST_PASSWORD"
J="Content-Type: application/json"
curl -s -o /dev/null -w "health: %{http_code}\n" "$B/health"
curl -s -o /dev/null -w "read without key: %{http_code}\n" "$B/api/orgs"
curl -s -o /dev/null -w "read with key: %{http_code}\n" -u "$A" "$B/api/orgs"
ORG=$(curl -s -u "$A" "$B/api/orgs" | sed -n 's/.*"id":\([0-9][0-9]*\),"domain".*/\1/p' | head -1)
WS=$(curl -s -u "$A" -X POST -H "$J" -d '{"name":"agent-check"}' "$B/api/orgs/$ORG/workspaces" | tr -d '"')
DOC=$(curl -s -u "$A" -X POST -H "$J" -d '{"name":"check"}' "$B/api/workspaces/$WS/docs" | tr -d '"')
curl -s -o /dev/null -u "$A" -X POST -H "$J" -d '{"tables":[{"id":"Prices","columns":[{"id":"Qty","fields":{"type":"Numeric"}},{"id":"Total","fields":{"type":"Numeric","isFormula":true,"formula":"$Qty * 2"}}]}]}' "$B/api/docs/$DOC/tables"
curl -s -o /dev/null -u "$A" -X POST -H "$J" -d '{"records":[{"fields":{"Qty":21}}]}' "$B/api/docs/$DOC/tables/Prices/records"
if curl -s -u "$A" "$B/api/docs/$DOC/tables/Prices/records" | grep -q '"Total":42'; then echo "python formula: ok"; else echo "python formula: FAILED"; fi
curl -s -o /dev/null -w "write without key: %{http_code}\n" -X POST -H "$J" -d '{"name":"intruder"}' "$B/api/orgs/$ORG/workspaces"
curl -s -o /dev/null -w "write with wrong key: %{http_code}\n" -u "admin:wrong" -X POST -H "$J" -d '{"name":"intruder"}' "$B/api/orgs/$ORG/workspaces"
curl -s -o /dev/null -w "spoofed identity header: %{http_code}\n" -H "X-Forwarded-User: admin@example.com" "$B/api/orgs"
```

A fresh deploy prints `health: 200`, `read without key: 401`, `read with key: 200`, `python formula: ok`, `write without key: 401`, `write with wrong key: 401` and `spoofed identity header: 401`.

## Why Deploy Grist on Railway?

Railway is a singular platform to deploy your infrastructure stack. Railway will host your infrastructure so you don't have to deal with configuration, while allowing you to vertically and horizontally scale it.

By deploying Grist on Railway, you are one step closer to supporting a complete full-stack application with minimal burden. Host your servers, databases, AI agents, and more on Railway.

- **Agent-ready** - no deployer fields, a contract block and one script that proves a document with a computed Python formula and the refusal of requests with no or a wrong password.
- **Not an open spreadsheet server** - grist-core has no login of its own; the gateway adds one before anything is reachable.
- **Data survives redeploys** - documents and the home database live on a volume at `/persist`.
- **Formulas work out of the box** - the pyodide sandbox is enabled, so Python formula columns calculate without extra privileges.
- **Pinned** - the upstream image is pinned by digest and the wrapper by tag and digest.

## Common Use Cases

- **Team database and tracker** - linked tables, views, filters and forms instead of a shared spreadsheet that breaks.
- **Agent-driven data store** - give an agent one authenticated REST API for structured records with computed columns.
- **Internal tools and dashboards** - inventory, CRM, content calendars and reports built from tables and Python formulas.
- **Spreadsheet replacement** - import CSV or Excel files and keep the data on infrastructure you control.

## Dependencies for Grist Hosting

- The grist service created by the template, with its volume at `/persist`. A custom domain is optional.

### Deployment Dependencies

- [Grist repository](https://github.com/gristlabs/grist-core)
- [Wrapper image source](https://github.com/bon5co/grist-railway)


## Similar templates

- [Rocky Linux](https://railway.com/deploy/rocky-linux) — Hosted Rocky Linux 9 workspace with SSH and persistent storage. 🚀
- [Foundry Virtual Tabletop](https://railway.com/deploy/X5tR6G) — A Self-Hosted & Modern Roleplaying Platform
- [Letta Code Remote](https://railway.com/deploy/letta-code-remote) — Run a Letta Code agent 24/7. No inbound ports, just deploy.

Open this page in a browser: https://railway.com/deploy/grist-or-just-updated-self-hosted-spread
