# 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

```sh
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](https://github.com/LiamBindle/readthis.md/blob/main/readthis.example.json) is a public server's.

| Field | Default | What it is |
|---|---|---|
| `base_url` | `http://localhost:8080` | the 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 |
| `contact` | none | where people reach you: every page's footer and every mail's Reply-To |
| `login` | anybody | `allow`: who may sign in, create documents and be shared with, as patterns (`you@example.com`, `*@example.org`); `refusal`: what anybody else is told |
| `admins` | nobody | who may open `/analytics` |
| `mail` | printed to the log | `host`, `port` (587; 465 is implicit TLS, any other STARTTLS, the certificate always verified), `from`, and `user`, whose password is `.secrets/smtp-password` |
| `tiers` | no limits | `free` 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 |
| `hosted` | nothing sold | sells 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:

```sh
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:

```caddyfile
{
    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:

```sh
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](/docs/agents) 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:

```sh
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:

```sh
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:

```sh
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

```sh
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 …`.

| Command | What it does |
|---|---|
| `person show EMAIL` | what the server holds about a person |
| `person tier EMAIL paid` | puts a person on the paid tier without paying; `free` takes it back |
| `person delete EMAIL` | deletes everything of theirs, as **Delete everything** does |
| `mail-test --to EMAIL` | sends one test message |
| `healthcheck` | checks `/readyz`; the image's health check |
| `backup` | writes a consistent snapshot to `/backup` |
| `verify --restored` | checks a restored database before it is served |
| `version` | prints 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.
