Files
verdande/docs/self-hosting.md
T
Kristian WindandClaude Opus 5 7c9bde2cfd docs(phase6): documentation site, landing page and open-source scaffolding
Material for MkDocs under docs/, covering self-hosting, the Rune, the
quick-add and filter languages, importing from Todoist, CalDAV,
mail-to-task, MCP, the AI layer and the API. The version is pinned: the
maintainers have announced that MkDocs 2.0 removes the plugin system
with no migration path, and an unpinned install would break this build
on the day it lands.

The landing page shows the quick-add parser as actual text rather than a
screenshot of it — that is the thing worth showing, and it reads better
as the real thing. It shares the app's design tokens, so the site and
what it describes look like one project.

MIT licence, contributing guide, security policy and issue templates.
Pages deployment is behind workflow_dispatch, so the docs build on every
push but nothing is published until the owner chooses to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 08:08:36 +02:00

109 lines
3.0 KiB
Markdown

# Self-hosting
verdande is one static binary and one directory. There is no database server, no
cache, no queue and no second process.
## Docker
```bash
docker run -d --name verdande \
--restart unless-stopped \
-p 8080:8080 \
-v verdande-data:/data \
-e VERDANDE_BASE_URL=https://todo.example.dk \
ghcr.io/kristianwind/verdande:latest
```
`VERDANDE_BASE_URL` has to be the address you actually reach it on. Invite links,
password resets and calendar feeds are all built from it, so getting it wrong
means the links in your email point somewhere that does not answer.
## Docker Compose
```yaml
services:
verdande:
image: ghcr.io/kristianwind/verdande:latest
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- ./data:/data
environment:
VERDANDE_BASE_URL: https://todo.example.dk
VERDANDE_SMTP_HOST: mail.example.dk
VERDANDE_SMTP_USER: verdande@example.dk
VERDANDE_SMTP_PASS: ${SMTP_PASSWORD}
VERDANDE_SMTP_FROM: verdande@example.dk
```
## Behind a reverse proxy
verdande expects to be terminated by something else. It reads `X-Forwarded-For`,
so rate limiting and the activity log see the real caller rather than the proxy.
=== "Caddy"
```
todo.example.dk {
reverse_proxy localhost:8080
}
```
=== "Cloudflare Tunnel"
```yaml
ingress:
- hostname: todo.example.dk
service: http://localhost:8080
- service: http_status:404
```
The WebSocket needs no special handling; Cloudflare proxies it as-is. A silent
socket is closed after 100 seconds, which is why verdande pings every 30.
!!! warning "Use HTTPS"
Over plain HTTP the session cookie cannot be `Secure`, and verdande will not
use the `__Host-` prefix that stops a subdomain overwriting it. Fine on a
laptop; not fine on anything reachable from outside.
## First run
Open the address and create an account. The first one is the administrator, and
the endpoint that creates it refuses once any account exists — so there is no
window in which somebody else can claim your instance.
Everybody after that joins by invite.
## What lives in `/data`
```
/data
├── verdande.db SQLite, WAL mode
├── files/ attachments, content-addressed
└── backups/ nightly snapshots, 14 kept
```
Back up the whole directory.
!!! danger "Do not copy verdande.db on its own"
It runs in WAL mode, so recent writes live in `verdande.db-wal` until they are
checkpointed. A copy of the `.db` file alone can be missing them. Copy the
whole directory, or take one of the nightly snapshots from `backups/` — those
are written with `VACUUM INTO` and are complete on their own.
## Backups
A snapshot is written once a day and the most recent fourteen are kept. They are
counted rather than aged out, so a container that was off for a month comes back
with its backups intact.
## Updating
```bash
docker compose pull && docker compose up -d
```
Migrations run at startup, each in its own transaction. Take a copy of `/data`
first anyway.