A template for self-hosting Grist with traefik and docker compose

Running Grist on your own computer is pretty easy. Hosting it to share with others requires a few more steps, such as hooking up an authentication method, wrangling certificates, and configuring a reverse proxy. I wanted to show one complete recipe for doing so.

Here is a docker-compose.yml file that does the following:

  • Uses the letsencrypt service to get a certificate for your domain.
  • Runs Grist behind a reverse proxy called traefik.
  • Connects Grist to authentication middleware called traefik-forward-auth. You can configure this middleware to perform Google/OpenID oauth based login.
version: '3'

services:
  reverse-proxy:
    # Use Traefik for routing and certificate handling.
    image: traefik:v2.6
    command:
      - --providers.docker
      - --certificatesResolvers.letsencrypt.acme.email=${EMAIL}
      - --certificatesResolvers.letsencrypt.acme.storage=/acme/acme.json
      - --certificatesResolvers.letsencrypt.acme.tlschallenge=true
      - --entrypoints.websecure.address=:443
      - --entrypoints.websecure.http.tls=true
      - --entrypoints.web.address=:80
      - --entrypoints.web.http.redirections.entrypoint.to=websecure
      - --entrypoints.web.http.redirections.entrypoint.scheme=https
    ports:
      - "80:80"
      - "443:443"
    volumes:
      # You may want to put state somewhere other than /tmp :-)
      - /tmp/grist/acme:/acme
      # Traefik needs docker access when configured via docker labels.
      - /var/run/docker.sock:/var/run/docker.sock

  traefik-forward-auth:
    # Authentication middleware.
    # See https://github.com/thomseddon/traefik-forward-auth for
    # options for configuring it.
    image: thomseddon/traefik-forward-auth:2
    environment:
      PROVIDERS_GOOGLE_CLIENT_ID: your-google-client-id
      PROVIDERS_GOOGLE_CLIENT_SECRET: your-google-client-secret
      SECRET: something-random
      LOGOUT_REDIRECT: "https://${DOMAIN}/signed-out"
    labels:
      traefik.http.services.traefik-forward-auth.loadbalancer.server.port: 4181
      traefik.http.middlewares.traefik-forward-auth.forwardauth.address: "http://traefik-forward-auth:4181"
      traefik.http.middlewares.traefik-forward-auth.forwardauth.authResponseHeaders: "X-Forwarded-User"

  grist:
    image: gristlabs/grist
    environment:
      GRIST_FORWARD_AUTH_HEADER: X-Forwarded-User
      GRIST_FORWARD_AUTH_LOGOUT_PATH: _oauth/logout
      GRIST_SINGLE_ORG: grist  # alternatively, GRIST_ORG_IN_PATH: "true" for multi-team operation
      GRIST_DEFAULT_EMAIL: ${EMAIL}
      APP_HOME_URL: https://${DOMAIN}
    ports:
      - "8484:8484"
    volumes:
      # You may want to put state somewhere other than /tmp :-)
      - /tmp/grist/data:/persist
    labels:
      traefik.http.services.grist.loadbalancer.server.port: 8484

      # When logging in, use traefik-forward-auth middleware.
      traefik.http.routers.login.rule: Host(`${DOMAIN}`) && PathPrefix(`/auth/login`)
      traefik.http.routers.login.middlewares: traefik-forward-auth
      traefik.http.routers.login.service: grist
      # Comment out each line with "letsencypt" in it if your domain is not publically
      # accessible and you want to use a self-signed certificate.
      traefik.http.routers.login.tls.certresolver: letsencrypt

      # traefik-forward-auth middleware itself has some internal endpoints.
      traefik.http.routers.auth.rule: Host(`${DOMAIN}`) && PathPrefix(`/_oauth`)
      traefik.http.routers.auth.middlewares: traefik-forward-auth
      traefik.http.routers.auth.service: grist
      traefik.http.routers.auth.tls.certresolver: letsencrypt

      # Otherwise, the middleware is not needed and would prevent
      # public shares. Grist will redirect to login when needed.
      traefik.http.routers.general.rule: Host(`${DOMAIN}`)
      traefik.http.routers.general.service: grist
      traefik.http.routers.general.tls.certresolver: letsencrypt

The template expects you to set DOMAIN and EMAIL environment variables. The email you set will be used when auto-applying for a certificate, and will also be configured as the initial site owner. If using Google logins, you’ll need to set PROVIDERS_GOOGLE_CLIENT_ID and PROVIDERS_GOOGLE_CLIENT_SECRET with your own values, see provider setup documentation.

Once you are satisfied, you can just run docker-compose up to bring up the containers, and Grist should become available at your domain.

Other tweaks you can do when it is working:

  • Change where you want to store data in the volumes sections (probably not /tmp like in this template!).
  • Turn on sandboxing by adding GRIST_SANDBOX_FLAVOR=gvisor in the environment section for Grist if you plan on working with untrusted documents.

A lot more authentication options are possible by using Grist’s support for SAML via Authentik, Keycloak, Auth0 etc. There are some notes for Authentik at Grist core multi user docker setup. There is more flexibilty but more opportunities for misconfiguration than this (relatively) short traefik-based template.

6 Likes

I used these instructions to create a public StackScript on Linode.com hosting:

This lets you start your own Grist instance on Linode using only a few steps:

Feedback welcome!

5 Likes

Hi Paul,

thank you. I want to use this configuration, but with Caddy server and Authelia for authentication. Caddy supports forward_auth since Friday by default.

Where is /auth/login called in Grist? “Sign in” calls /signin?next=%2F on my Grist installation.

When there is no Sign in called for now because I open /, an empty Remote-Email header will be send to Grist. Grist then use GRIST_DEFAULT_EMAIL or you@example.com. How to avoid this? Trying GRIST_SUPPORT_ANON false or true doesn’t help.

Configuring authentication for the whole site /* works as expected, but as you wrote public sharing can’t work.

Thank you and best regards

Helmut

1 Like

I am happy to setup a dockerized sample of Grist via Caddy2 and Authentik.

Is this something you can test for me and let me know where it falls short of the usecase?

Hi Helmut, /auth/login is called if Grist is configured to use forward auth - in other words, if GRIST_FORWARD_AUTH_HEADER is set (see grist-core/ForwardAuthLogin.ts at 52eb5325c23f1015fd94cd7eed5b66eccff01dce · gristlabs/grist-core · GitHub). You can see the code for the call here:

If you are seeing GRIST_DEFAULT_EMAIL being used, it sounds like Grist isn’t using the forward auth login mechanism, but is still using the default “minimal” login mechanism implemented in grist-core/MinimalLogin.ts at 52eb5325c23f1015fd94cd7eed5b66eccff01dce · gristlabs/grist-core · GitHub

Can you double check that GRIST_FORWARD_AUTH_HEADER is set, and also optionally GRIST_FORWARD_AUTH_LOGOUT_PATH? If you get it working, I’d be happy to hear about what worked for you.

1 Like

Hello Paul,

I think it should be ok. This is my env file:

TZ=Europe/Berlin

APP_DOC_URL=https://grist.example.com
APP_HOME_URL=https://grist.example.com
PORT=8485

GIST_INST_DIR=/persist
GRIST_DATA_DIR=/docs

GRIST_SINGLE_ORG=docs
GRIST_ORG_IN_PATH=false
GRIST_SUPPORT_ANON=false
#GRIST_DEFAULT_EMAIL=services@example.com

GRIST_SANDBOX_FLAVOR=unsandboxed

GRIST_PROXY_AUTH_HEADER=Remote-Email
GRIST_FORWARD_AUTH_LOGOUT_PATH=/signed-out

#DEBUG=1

I’m using gristlabs/grist:0.7.8 Docker image.

If I enable /auth/login only for authentication, pressing sign-in, Grist login with you@example.com and don’t see any /auth/login call.

Thank you and best regards,

Helmut

Edit: Oh no!!! Now I found the problem after looking to your text and my env file. Have always used GRIST_PROXY_AUTH_HEADER, but it’s GRIST_FORWARD_AUTH_HEADER. Now it’s working.

I think I lost 3h of my life last weekend and I think I’m using PROXY because of this thread:

Thank’s for replying. I think I never noted the difference, because the word PROXY makes also sense.

Argh, sorry for your pain! These are too easy to confuse.

Glad things are working now at least.

1 Like

thanks a lot, it’s working well :grin:
Except for the : Examples & Templates
I have this msg : error: “organization not found”
Should it work ?
And is it normal that when I invite a person(gmail account) with it, no email is sent?

The examples & templates link isn’t configured well in self-hosted Grist, we should probably just hide it or have it link to https://templates.getgrist.com/

That’s normal, some configuration would be needed to have a way to send emails. We use sendgrid ourselves, and haven’t built a more general solution yet. There’s a thread about that here: feature request: smtp settings for self hosted installation · Issue #146 · gristlabs/grist-core · GitHub

1 Like

One thing to clarify. Regarding SSO. Let’s say you have three apps. All of them use auth on the reverse proxy: grist .example.com, whoami .example.com and whoami2 .example.com. On all of them the reverse proxy catch any /sign-out or /logout call and logout the session.

Following happens:

  • login with user1 on whoami .example.com. result: logged in with user1
  • open in another tab grist .example.com. result: need to press sign-in, no need to auth and also being logged in with user1
  • open in another tab whoami2 .example.com. result: logged in with user1
  • logout on whoami .example.com/logout. result: logged out, forced to login again with Authelia
  • switch to the tab with whoami2 .example.com, reload the page. result: because I’m still logged out, there is also Authelia’s login screen

For now it’s ok. Maybe grist should recognize the email header and don’t need to press sign-in first. But then something strange happens:

  • switch to the tab with grist, reload the page. result: still logged in on grist with user1!

It becomes even more strange:

  • still logged out. now login with user2 on whoami. result: logged in with user2
  • switch to the tab with whoami2, reload the page. result: logged in with user2
  • switch to the tab with grist, reload the page. result: still shows user1 logged in and it’s possible to open documents from user1!

Could this be an error in grist? At least with caddy’s forward_auth implementation, only the /auth/login call send the email header to grist. I think this should be the same with your traefik configuration?

Sounds like a cookie needs clearing upon logout. Thanks for reporting!

1 Like

Should I create an issue on GH? Thank you.

Actually that would be great, just to get the scenario clear with what domains the cookies are being set on, and the treatment of signout urls.

Hello :grinning:,
With that docker compose,
how can we install libraries like jinja for example ?
Thanks

1 Like