dodo
A self-hosted todo & reminders service. MIT licensed.
Tasks support one-off reminders at a specific date/time and recurring
tasks (daily/weekly/monthly/yearly). Notifications repeat at the
priority's interval until completed (low=2h, normal=1h, high=20m) and
are delivered through each user's own Telegram bot (with a
"Complete" button straight from the chat) and as desktop browser
notifications while the web app is open. Tasks can be snoozed to
silence reminders until later. The service is designed to run inside a
private Tailscale tailnet and uses long polling (no webhooks).
All users are equal — there are no roles. User and token management is
done with the dodo admin CLI (direct DB access); everything else is
per-user and scoped to the authenticated user.
| Binary |
Purpose |
dodo |
HTTP API + web UI + scheduler + telegram pollers; admin CLI. Ships in the container. |
dodo-cli |
AI-agent CLI client (JSON stdout, --pretty for humans). |
dodo-tui |
Terminal UI client (API + token auth). |
Quickstart (docker)
docker run -d --name dodo \
-p 8080:8080 \
-v dodo-data:/data \
-e DODO_ENCRYPTION_KEY=$(openssl rand -base64 32) \
ghcr.io/mtzanidakis/dodo
# create the first user (inside the container; prompts for the password)
docker exec -it dodo dodo admin user create --email you@example.com
TOK=$(docker exec -it dodo dodo admin token create --email you@example.com --name agent | jq -r .token)
dodo-cli init --url http://localhost:8080 --token "$TOK"
dodo-cli tasks create --title "Pay electric bill" --due 2026-07-11T17:00:00Z --priority high
dodo-cli tasks list
Quickstart (local, from source)
mise install
mise run build-all
export DODO_ENCRYPTION_KEY=$(openssl rand -base64 32)
export DODO_DATABASE_PATH=/tmp/dodo.sqlite
./dodo admin user create --email admin@example.com # prompts for the password
./dodo serve &
TOK=$(./dodo admin token create --email admin@example.com --name agent | jq -r .token)
./dodo-cli --url http://localhost:8080 --token "$TOK" tasks create --title "Pay bill" --due 2026-07-11T17:00:00Z --priority high
./dodo-cli --url http://localhost:8080 --token "$TOK" tasks list
Open http://localhost:8080/login in a browser and sign in with those
credentials to use the web UI: a list view (with today/this-week/
this-month and pending/completed/all filters), a month calendar,
per-user profile, API token management, Telegram setup, dark/light/
system theme, and English/Greek locales.
Deploy with Docker Compose
The repo ships a compose.yaml. Configure the encryption key and start it:
cp .env.example .env && chmod 600 .env
# edit .env: set DODO_ENCRYPTION_KEY (openssl rand -base64 32)
docker compose up -d
# first user + a CLI token (user create prompts for the password)
docker compose exec dodo dodo admin user create --email you@example.com
docker compose exec dodo dodo admin token create --email you@example.com --name agent
The web UI is at http://localhost:8080.
Production (Tailscale, no exposed ports)
compose.override.yaml-prod fronts dodo with
tsrp, a Tailscale reverse proxy, and
removes the published port so the service is reachable only on your tailnet.
Activate the override and set the Tailscale vars:
ln -s compose.override.yaml-prod compose.override.yaml # compose auto-merges compose.override.yaml
# in .env: HOSTNAME=dodo and TS_AUTHKEY=tskey-auth-...
docker compose up -d
dodo is then served at https://dodo.<your-tailnet>.ts.net. The override also
bind-mounts ./dumps and adds hourly dodo backup labels for
ofelia — run an ofelia scheduler
alongside the stack to execute them.
Server env vars
See AGENTS.md for the full table. Highlights:
| Env |
Default |
Notes |
DODO_DATABASE_PATH |
/data/dodo.sqlite |
SQLite path (serve + admin). |
DODO_LISTEN |
:8080 |
HTTP bind. |
DODO_ENCRYPTION_KEY |
(required) |
32-byte base64; AES-256-GCM for telegram bot tokens. |
DODO_SCHEDULER_INTERVAL |
1m |
Reminder scan cadence (min 1m). |
The CLI/TUI clients ignore env vars and read
~/.config/dodo/config.json (overridable with --url/--token/--config).
Client config (dodo-cli / dodo-tui)
{
"url": "http://localhost:8080",
"token": "dodo_xxxxxxxxxxxx",
"log_level": "info",
"timezone": "Europe/Athens"
}
dodo-cli init --url <api> --token <token> [--timezone <IANA>] writes the
config for first-time setup.
Both clients render task times (and the CLI parses --due/--until input)
in your timezone, matching the web UI. The zone is resolved in this order:
the optional timezone config value (an IANA name like Europe/Athens, or
UTC), then your profile timezone from the server, then the host's local
zone. The CLI keeps its JSON output valid RFC3339 — same instant, just a local
offset (e.g. 2026-07-11T20:00:00+03:00) instead of a Z suffix.
Telegram setup
- Create a bot with @BotFather and copy its
token.
- In the web UI Account page (or
POST /api/v1/me/telegram), save the
bot token and your Telegram user id (comma-separated list of allowed
ids).
- Send
/start to your bot from Telegram; the chat gets linked.
- Reminders now arrive via your own bot with a Complete button.
Bot tokens are encrypted at rest with AES-256-GCM
(DODO_ENCRYPTION_KEY). The server receives updates with long polling
(no webhooks needed).
Browser notifications
On the web Account page, click Enable notifications to also get a
desktop notification when a task is due. This uses the Web Notifications
API, so it fires while a dodo tab is open and requires a secure context
(localhost or https — not plain http to a LAN/tailnet IP).
Backup
The /data volume is the only state. Take a consistent online backup with the
built-in command (safe while the server is running — it uses VACUUM INTO):
dodo backup -dump /data/backup-$(date +%F).sqlite
Inside the container:
docker exec dodo dodo backup -dump /data/backup-$(date +%F).sqlite
-dump refuses to overwrite an existing file, so use a fresh (e.g. dated)
path. Equivalent with the sqlite3 CLI, if you have it:
sqlite3 /data/dodo.sqlite ".backup /data/backup.sqlite"
Upgrading the clients
dodo-cli and dodo-tui can update themselves in place from the latest GitHub
release:
dodo-cli version # print the installed version
dodo-cli upgrade # download & replace with the latest release, if newer
dodo-tui upgrade
upgrade compares the installed version with the latest release, and when a
newer one exists it downloads the matching archive for your OS/arch, verifies
its checksum, and atomically replaces the running binary (so the install
directory must be writable). Released binaries carry their version; a
locally-built binary reports its git describe string and only upgrades to a
strictly newer release.
Development
mise run lint # golangci-lint
mise run test # go test -race -cover ./...
mise run build-all # all three binaries
mise run web:build # frontend assets -> internal/web/dist
Commits follow Conventional Commits,
enforced by CI (feat fix refactor test chore docs ci build perf style).
License
MIT - see LICENSE.