readthis.mdDocsOpen the app

Self-hosting

readthis.md is free software (AGPL-3.0): one Go binary and one SQLite database, in one Docker image. You need Docker with Compose 2.24.4 or later and, on a public server, an SMTP service for mail.

Run it

git clone https://github.com/LiamBindle/readthis.md
cd readthis.md
echo '{"login": {"allow": ["you@example.com"]}}' > readthis.json
docker compose up --build -d

Open http://localhost:8080 and sign in. Without mail configured, the sign-in code is printed in docker compose logs readthis. Everything lives in the readthis-data volume; docker compose down -v deletes it.

readthis.json

The one configuration file. Unknown fields stop the server, naming them. readthis.example.json is a public server's.

FieldDefaultWhat it is
base_urlhttp://localhost:8080the public address, used in every mailed link. An https one marks cookies Secure, sends HSTS, and takes the client's address from the last X-Forwarded-For entry
contactnonewhere people reach you: every page's footer and every mail's Reply-To
loginanybodyallow: who may sign in, create documents and be shared with, as patterns (you@example.com, *@example.org); refusal: what anybody else is told
adminsnobodywho may open /analytics
mailprinted to the loghost, port (587; 465 is implicit TLS, any other STARTTLS, the certificate always verified), from, and user, whose password is .secrets/smtp-password
tiersno limitsfree and paid, each documents, unused_days, trash_days (30 by default), disk_bytes (the trash included) and emails_per_day, 0 being no limit; paid_addresses, patterns on the paid tier without paying
hostednothing soldsells the paid tier through Stripe, as readthis.md does. You don't need it to self-host

Secrets are files in .secrets/, mounted at /run/secrets and read only when the configuration needs them: smtp-password with mail.user, and stripe-key and stripe-webhook-secret with hosted. They belong to the container's user:

sudo chown -R 10001:10001 .secrets && sudo chmod 700 .secrets && sudo chmod 600 .secrets/*

After changing readthis.json or a secret, run docker compose up -d --force-recreate.

On a server

  1. Clone the repository, and write readthis.json and .secrets/.
  2. Run docker compose up --build -d. Compose binds readthis.md to 127.0.0.1:8080; keep it there.
  3. Put an HTTPS proxy in front. For Caddy on the host:
{
    servers {
        timeouts {
            read_body 5m
        }
    }
}

docs.example.com {
    reverse_proxy 127.0.0.1:8080 {
        flush_interval -1
    }
}

The proxy must append X-Forwarded-For, as Caddy and nginx's proxy_add_x_forwarded_for do: readthis.md limits sign-ins by its last entry, so port 8080 must be reachable only through the proxy. Keep the proxy's write and idle timeouts off or generous, since live updates and an agent waiting for an answer hold a request open.

Mail

Use your provider's SMTP details, verify the sending domain, and send yourself a test:

printf '%s' 'your-smtp-password' | sudo tee .secrets/smtp-password >/dev/null
sudo chown -R 10001:10001 .secrets && sudo chmod 600 .secrets/*
docker compose up -d --force-recreate
docker compose exec readthis readthis mail-test --to you@example.com

Agents

Agents connect to https://your.server/mcp, as Connect an agent shows; the Claude Code plugin takes --config readthis_url=https://your.server. Claude and ChatGPT connect from their own servers, so they need yours reachable from the internet, and your server fetches some agents' published descriptions, so it needs to reach the internet too.

Backups

A volume is not a backup. backup/readthis-backup takes an hourly snapshot while readthis.md runs and sends it, encrypted with restic, to an S3-compatible bucket with another provider. Never copy /data with cp or rsync while readthis.md runs.

The bucket needs versioning on, with noncurrent versions expiring after 14 days. The server's key may read and write objects but never delete a version or change the bucket's settings, so a compromised server can't destroy its backups. Keep your own key, which can, off the server.

  1. Make a restic password and keep it in your password manager. Put it in .restic-password, owned by 10001 with mode 600.
  2. Copy backup/env.backup.example to .env.backup and fill it in with the bucket and the server's key.
  3. Run docker compose run --rm restic init.
  4. Install the hourly timer:
sudo cp backup/readthis-backup.service backup/readthis-backup.timer /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now readthis-backup.timer
sudo systemctl start readthis-backup && journalctl -u readthis-backup -n 20

Forgetting old snapshots needs a key that can delete, so run it weekly from another computer, with your own key:

restic forget --host readthis --keep-within-hourly 2d --keep-within-daily 30d --prune

Restore

backup/readthis-restore with no argument lists the snapshots: choose one by its time and its documents= tag. Then:

backup/readthis-restore <snapshot>   # --replace moves the current data aside first
docker compose up -d

It verifies the database and leaves readthis.md's hourly sweep held, so nothing is mailed, trashed or deleted, until you remove /data/restored-from.json. Tell your users that anything after the snapshot is lost, then run docker compose exec readthis rm /data/restored-from.json.

On a new server, clone the repository, bring back readthis.json, .env.backup, .secrets/ and .restic-password from your password manager, restore as above, and install the timer. readthis.md refuses a database with another schema number, so build the version the snapshot's MANIFEST names, or a later one with the same schema.

Update

sudo systemctl start readthis-backup
git pull
docker compose up --build -d

An update restarts readthis.md, and open pages reconnect. Run one instance per volume, never two. readthis.md is in beta: an update that changes the database's schema says so, and needs a new database, so download what you need first.

Your own changes

Every page links the source of the build it runs, as the AGPL asks. If you run a modified readthis.md for other people, publish your source and set READTHIS_SOURCE_URL=<your repository> and READTHIS_VERSION=<commit> in .env before building. Please give it its own name, so nobody mistakes it for readthis.md.

Commands

Run each as docker compose exec readthis readthis ….

CommandWhat it does
person show EMAILwhat the server holds about a person
person tier EMAIL paidputs a person on the paid tier without paying; free takes it back
person delete EMAILdeletes everything of theirs, as Delete everything does
mail-test --to EMAILsends one test message
healthcheckchecks /readyz; the image's health check
backupwrites a consistent snapshot to /backup
verify --restoredchecks a restored database before it is served
versionprints the build

/healthz answers while the process runs, and /readyz also checks the database. Admins open /analytics by the week, month or year; add ?ref=name to a link to see what it brings in.

This page as Markdown: /docs/self-hosting.md · every page for agents: /llms.txt