Portainer Templates logo

Portainer Templates

OpenTodo OpenTodo

Container

Productivity

Open-source, local-first task manager. Works offline and syncs every device from one small container. Source: https://github.com/pixari/opentodo

Image details

Architecture: amd64, arm64
Image size: 13 MB
User: pixari

Source details

Stars: 114
Forks: 11
Language: TypeScript
License: AGPL-3.0
Updated: 19 minutes ago
Website: open-todo.app/

Configuration

Type
Container
Platform
linux
Image
ghcr.io/pixari/opentodo:stable
Ports
8080:8080/tcp
Volumes
/data
Env vars
OPENTODO_SETUP_TOKEN=OPENTODO_BASE_URL=OPENTODO_ALLOW_SIGNUP=falseOPENTODO_TRUSTED_PROXIES=
Restart
unless-stopped

Notes

Open http://<your-host>:8080 right after deploying: the first visitor creates the owner account (paste the setup token there if you set one), and sign-up is closed after that. Everything OpenTodo stores is in the /data volume, so back that volume up. On plain http, tasks, sync and sharing work; installing the app, offline use, push notifications and passkeys need https: put a reverse proxy with a certificate in front and set the public https address. Updates: re-pull the stable image (Recreate with Re-pull image). Docs: open-todo.app/docs/self-hosting/portainer

Standalone Install

Select an install method, to see config/commands for deploying OpenTodo

Installation method

Install on Portainer

Import all app templates into your Portainer instance, for easy 1-click deploys

  1. Ensure both Docker and Portainer are installed, and up-to-date
  2. Log into your Portainer web UI
  3. Under Settings → App Templates, paste the below URL
  4. Head to Home → App Templates, and the list of apps will show up
  5. Select OpenTodo, fill in any config options, and hit Deploy

Template Import URL

https://raw.githubusercontent.com/Lissy93/portainer-templates/main/templates.json
Show Me demo

More install options in our documentation, or see pixari/opentodo for app-specific guidance.

OpenTodo logo


OpenTodo


A to-do app that runs on your own server.
Plan your day, keep projects in order, set reminders and share lists with your family or your team. Works on your phone and your computer, even offline. No monthly plan per person, and your tasks stay with you.


License: AGPL-3.0 CI Latest release Container image Docker image size Spec-driven with OpenSpec Listed on Open Source Alternatives Sponsor on GitHub Buy me a coffee


Try the demo (no sign-up) · Install in 2 minutes · Read the docs · Website


If OpenTodo looks useful, a star on GitHub helps other people find it, and sponsoring or a coffee helps me keep building it.


Quick add in plain language with date, priority and project chips, a card moved across a project board, then a task added offline that syncs on reconnect

Try it in 30 seconds

  • No install: open the demo and press Try the demo. You get a private copy with sample data, deleted after a few hours.
  • On your machine or server:

```bash docker run -d -p 8080:8080 -v opentodo-data:/data ghcr.io/pixari/opentodo:stable ```
Open ; the first visit creates your account. No configuration needed. Coolify, Unraid, Compose and plain binaries are in Quick start.
Questions, ideas and bug reports are welcome in issues. To talk to other people who use OpenTodo, join the Discord server.

Today view with overdue and due-today tasks


Project view with the task detail panel Mobile dark mode


Day view: timed tasks as blocks sized by their estimate, overlapping blocks side by side, the unscheduled lane and the planned total against the day length

Why OpenTodo

  • No subscription, no lock-in. Free software under the AGPL. Nobody can paywall your task list or shut it down with the company.
  • Works offline, on every device. Each phone and computer keeps a full copy of your data (local-first), so the app works with no connection at all. The server syncs, backs up and shares; it is never in the way of an edit.
  • Your data, one folder. Everything lives in one /data volume around a single SQLite file. Export to JSON at any time, import it (or a CSV, or another app's export) on any instance, and schedule encrypted backups to disk, S3 or WebDAV.
  • Tiny footprint. One container, one static binary of about 23 MB with the web app inside, around 20 MB of memory when idle. Runs on a Raspberry Pi or the cheapest VPS. No database server, no Redis, no mandatory configuration.
  • Private by design. No telemetry, no analytics, no phone-home, no third-party scripts or fonts. The server only contacts what you configure (see Privacy).
  • Bring your own AI, if you want it. Plan and change tasks by talking to the assistant in the app (Anthropic, OpenAI, Gemini or a model on your own network), connect the assistant you already use over MCP, or let a coding agent drive the ot CLI. Off by default, every change recorded and undoable, no AI vendor built in. See Use AI with OpenTodo.
  • Keyboard-first. q to capture, j/k to move, c to complete, 1–4 for priority, g t for Today, ? for the full list, and optional Vim keybindings.
  • Built in the open with specs. Every feature starts as an OpenSpec change under openspec/, so behaviour is documented before it is built.

Status

OpenTodo is at 0.4: every feature on the roadmap is in, covered by automated tests, but some integrations still need real-world testing (CalDAV with specific apps, single sign-on providers, real phones). If you try it, tell us how it went in the call for testers.
How it's built: every feature starts as a written spec (OpenSpec), and much of the code was written with AI coding agents working from those specs. A strict CI checks every change: Go and web tests, spec validation, a phone-layout check in Chromium and WebKit, and accessibility checks. Everything is reviewed before it's merged, and the full history is public.

Features

The highlights: the app in six languages, quick add in plain language, projects with sections and boards, recurring tasks, reminders with push, filters and saved views, time blocking, shared projects with comments, CalDAV, webhooks, a REST API and CLI, passkeys, two-factor and single sign-on, optional end-to-end encryption.
Full feature list

Capture

  • The app in your language: English, German, French, Spanish, Italian or Portuguese, picked once and synced to every device; dates, notifications and quick add follow it. The guide stays in English. Language · Translating
  • Quick add from anywhere (q), with the view's context as defaults: today in Today, the current project in a project.
  • Natural language in six languages, offline: Call Sarah next Wed 3pm #work @call p1 +30m sets the date, time, project, label, priority and estimate, with dismissible preview chips. English, German, French, Italian, Spanish and Portuguese. Syntax
  • Voice capture (opt-in per device): dictate into quick add with the microphone button or Shift+V; "hashtag home priority two" becomes #home p2. Uses the browser's own speech recognition, or a speech-to-text service you run (Speaches, whisper.cpp) so dictation stays on your network. Voice
  • Share sheet, bookmarklet and browser extension: send a page from the phone's share menu, any desktop browser or the Chromium/Firefox extension in extension/ to a prefilled capture screen; the task keeps the page, or the GitHub, GitLab, Jira or Linear issue, as a chip that opens it. Works offline, holds no token. Capture
  • Email to task: forward a mail to your secret address (todo+…@example.com) and it lands in the Inbox. Optional; the operator points OpenTodo at one IMAP mailbox. Email
  • GitHub and GitLab issues as tasks: connect an account with a personal access token and the open issues assigned to you become tasks that complete when the issue closes; nothing is written back. Off until the owner enables it. Issue import
  • Command line: ot add "Call the plumber" --due tomorrow, ot today, --json for scripts. CLI

Organisation and views

  • Inbox, Today, Upcoming, Completed and projects with colours, nested to any depth, with rolled-up counts.
  • Subtasks to any depth with done/total progress; Tab and Shift+Tab indent and outdent.
  • Sections and Kanban board per project (v toggles list and board), drag and drop on mouse and touch, or Shift+←/→.
  • Manual ordering by drag and drop or Alt+↑/↓, convergent across devices.
  • Labels across projects with colours, a type-ahead picker (L), per-label views and merging of duplicates.
  • Filters: one-line queries such as @work & due <= tomorrow & p1, evaluated on the device, live and offline. Filters
  • Saved views and dashboards: pin a filter to the sidebar with live counts, choose grouping and sort, combine up to six views on one dashboard. Saving views
  • Rich-text notes with headings, lists, check lists, links and Markdown shortcuts; edits from several devices or people merge character by character, offline too. Scripts and integrations keep reading the note as Markdown.
  • Trash and history: restore deleted tasks and projects, browse each item's change history with "restore this value", empty the Trash for good; old items are purged after 30 days. Projects whose tasks are all done are archived automatically. Purge
  • Vim keybindings (opt-in per device): modes, counts, gg/G, dd and a : command line.

Time

  • Floating due dates with optional time, the same day and time wherever you are; a timed task can be fixed to a time zone (z) for meetings and flights. See Floating vs fixed times.
  • Recurring tasks with rules you can read: every weekday, every last weekday, every 3 days after completion, until 2026-12-31, for 10 times; skip or detach one occurrence. Recurrence
  • Reminders: any number per task, relative to the due time or at a fixed moment, with snooze and dismiss. In the app offline, and as web push to devices that opt in, with no vendor account. Reminders
  • Time blocking: duration estimates (e), Day and Week calendars (g d, g w) with a 15-minute grid, drag to schedule or resize, planned hours against your day length, full keyboard control.

Collaboration

  • Shared projects with the accounts on your server as viewer, editor or admin (Shift+S); members edit offline and converge like your own devices; roles are enforced by the server. Sharing
  • Assignees (Shift+A), an Assigned to me view and in-app notifications. Assignment
  • Threaded Markdown comments with @mentions, and a per-task and per-project activity log derived from the sync history. Comments
  • Activity feed (g h) across every shared project, with unread counts that sync between your own devices only. Activity feed
  • Invitations and user administration: single-use invitation links, admin and member roles, deactivation, reset links you send yourself, owner transfer. Adding people

Integrations and data

  • REST API and API tokens (full or read-only) that write through the same op log as the app. API
  • Calendar apps (CalDAV): two-way sync with Apple Reminders, Thunderbird and Android tasks apps via DAVx⁵, with per-device app passwords. Off until the owner enables it. CalDAV
  • AI assistant in the app: plan and change your tasks by talking (Shift+I), with Anthropic (Claude), OpenAI, Gemini or a model on your own network, using your own key. Off until the owner enables it; each person accepts what is sent first; every change is proposed for review and undone in one step; conversations stay on the device. AI assistant
  • AI assistants (MCP): a read-only Model Context Protocol endpoint so an assistant can answer "what is on my plate today?". Off until the owner enables it. MCP
  • Claude Code plugin: /plugin marketplace add pixari/opentodo-claude connects Claude Code to your server with skills that use it safely. Claude Code
  • Webhooks on task and project events for Home Assistant, n8n or a Telegram relay: signed, retried, logged, with private-network protection. Webhooks
  • Import the app's own export, CSV or other task managers' exports (Todoist, TickTick, Things, Linear, Google Tasks, Trello, Asana, Microsoft To Do, Apple Reminders and Any.do), with a preview, duplicate detection and undo. Import
  • Export everything as JSON from Settings or GET /api/v1/export.
  • Markdown vault: ot vault keeps a folder of Markdown files in two-way sync, one file per project in the Obsidian Tasks format. Vault
  • Scheduled backups of the whole server to a folder, S3-compatible storage or WebDAV, with retention, optional encryption and opentodo restore. Backups

Security and privacy

  • Closed sign-up by default after the owner account is created; argon2id passwords, HttpOnly sessions, strict origin checks, CSP, rate limits.
  • Passkeys (WebAuthn) with browser autofill; needs an https OPENTODO_BASE_URL.
  • Two-factor authentication (TOTP) with recovery codes, "remember this device", an owner policy to require it, and step-up confirmation before sensitive changes.
  • Single sign-on with your own OpenID Connect provider (Authentik, Keycloak, Authelia, Zitadel…), including an SSO-only mode with owner break-glass. SSO
  • Audit log of sign-ins, credentials, invitations, exports and setting changes; never task content, client addresses only if the owner opts in. What is logged
  • Optional end-to-end encryption per account: titles, notes, names and comments are encrypted on your devices, so the server, backups and integrations hold only ciphertext. Threat model

Everywhere

  • Offline and installable: every change is applied locally first and queued; the app loads and works with no network.
  • Live sync: field-level, conflict-free sync across devices with no "pick a version" dialogs; a change shows up on your other devices within a second or two, with polling as the fallback. Sync protocol

Floating vs fixed times

A due date such as "pay rent on the 1st" or "call mum Sunday 18:00" is floating: it means that calendar day and clock time wherever you are, so it never moves when you travel. That is the default for every task.
Some times are instants: a stand-up at 09:00 Berlin time, a flight that departs at 14:35 in Lisbon. Open the task, set a time and tick Fixed to time zone (or press z): the time is pinned to your current zone. A device in Los Angeles then shows the stand-up at 00:00 with 09:00 Berlin as a hint, and Today, Upcoming, sorting, filters, reminders and the calendar all use the converted time, across daylight-saving changes. The API takes the same shape: "due":{"date":"2026-10-11","time":"09:00","tz":"Europe/Berlin"}.

Quick start

No environment variable is required. Open the app, create the owner account, and you are done; everything else is optional.

Docker

docker run -d --name opentodo -p 8080:8080 -v opentodo-data:/data \
  ghcr.io/pixari/opentodo:stable

Open and create the owner account. Opened from outside your local network, the setup screen asks for a one-time setup code: find it with docker logs opentodo (details). Behind a reverse proxy, add -e OPENTODO_BASE_URL=https://todo.example.com (needed for Secure cookies, passkeys and web push). The image is published for linux/amd64 and linux/arm64. :0.6 follows the 0.6 releases; :latest follows the development branch. Details: docs/self-hosting/docker.md.

Docker Compose

git clone https://github.com/pixari/opentodo.git
cd opentodo
cp .env.example .env          # optional, every value has a default
docker compose up -d

Data is kept in the opentodo-data volume. Setting up from outside your local network needs the setup code from docker compose logs opentodo.

Coolify

  1. New Resource → Public Repository, URL https://github.com/pixari/opentodo, build pack Docker Compose.
  2. Assign a domain to the opentodo service; Coolify provisions HTTPS and passes the URL to the app.
  3. Deploy, open the domain, create the owner account.

Or paste the one-click template deploy/coolify/opentodo.yaml as a Docker Compose resource (pending in Coolify's built-in service list). Details, including building from your fork: docs/self-hosting/coolify.md.

Unraid

OpenTodo is in Community Applications: open Apps, search for OpenTodo and click Install, then open the WebUI and create the owner account. The template lives in pixari/opentodo-unraid, published from deploy/unraid/. Details: docs/self-hosting/unraid.md.

Other platforms

Templates for CapRover, CasaOS and ZimaOS, Cosmos Cloud, Dokploy, Easypanel, Fly.io, Home Assistant (add-on), Kubernetes (Helm), Podman, Portainer, Render, Runtipi, Synology, TrueNAS and Umbrel are in deploy/, each with a guide: CapRover · CasaOS · Cosmos · Dokploy · Easypanel · Fly.io · Home Assistant · Kubernetes · Podman · Portainer · Render · Runtipi · Synology · TrueNAS · Umbrel. Until a platform's catalog lists OpenTodo, the guide shows how to install it as a custom app.

Single binary

Download opentodo-<os>-<arch> from the latest release (Linux, macOS and Windows; check it with SHA256SUMS), or build it yourself:
chmod +x opentodo-linux-amd64 && ./opentodo-linux-amd64   # serves on :8080, data in ./data
# or from source (needs Go 1.26+ and Node 22+):
make build && ./bin/opentodo

The binary is fully static (CGO_ENABLED=0, pure-Go SQLite) and also offers opentodo restore, opentodo mfa-reset, opentodo reset-owner-password, opentodo healthcheck and opentodo version.

Command line client

ot talks to any OpenTodo server over the REST API with a token it creates at login. It is a separate static binary (release assets for Linux, macOS and Windows, or go install github.com/pixari/opentodo/cmd/ot@latest) and is not part of the container image.
ot login https://todo.example.com
ot add "Call the plumber" --project Home --due tomorrow --time 09:00 --priority 2
ot today
ot done 8c2a                  # full id or unique prefix
ot list --json | jq '.[].title'

Full reference: docs/cli.md.

Configuration

Every setting is an optional environment variable with a safe default; an invalid value stops startup with a message naming the variable. The container image sets OPENTODO_ADDR=:8080 and OPENTODO_DATA_DIR=/data. See .env.example for a commented template.
VariableDefaultPurpose
OPENTODOADDR:8080Listen address
OPENTODODATADIR./data (binary), /data (image)Directory for the SQLite database, secrets, the VAPID key and local backups
OPENTODOBASEURLunset (inferred from requests)Public URL. With https: Secure cookies, Strict-Transport-Security, passkeys and web push; also used for strict origin checks (the request must be addressed to its host) and in invitation, reset and CalDAV links
OPENTODOSETUPTOKENunsetWhen set (16+ characters), creating the first account requires this token from every address, so a fresh public server cannot be claimed by a stranger. Unset, setup from a public address needs the one-time setup code printed in the server log instead. No effect once the owner exists. The Coolify template generates one
OPENTODOALLOWSIGNUPfalseLet anyone create an account after the owner exists. Prefer invitations
OPENTODOSESSIONDAYS90Browser session lifetime in days
OPENTODOLOGLEVELinfodebug, info, warn or error. The startup line shows the level in use (loglevel=); debug adds one line per API request (method, path, status, duration)
OPENTODOIMPORTMAXMB20Largest import file, 1–200
OPENTODOATTACHMENTSMAXFILEMB25Optional preset for the largest attachment, 1–2048; applied once, then Settings decides
OPENTODOATTACHMENTSQUOTAMB1024Optional preset for each account's attachment total, 0 (unlimited)–1048576; applied once
OPENTODOTRASHRETENTIONDAYS30Days a deleted task or project stays in the Trash before it is purged, 0–3650; 0 never purges automatically. See Purge
OPENTODODEMOunset (off)true turns a dedicated instance into a public demo: a private, seeded sandbox per visitor, deleted after 2 h of inactivity, after 24 h and every night at 03:00 UTC; abusable features refused with 403 demorestricted
OPENTODODEMOMAXSANDBOXES, OPENTODODEMOMAXDBMB500, 1024Optional demo tuning: live sandbox cap and the database size above which no new sandbox is created
OPENTODOTRUSTEDPROXIESloopback and private networksOptional: the proxies whose X-Forwarded-For, X-Forwarded-Host and X-Forwarded-Proto are believed, for rate limits, the audit log and the setup code (IPs, CIDRs, private for the default ranges, cloudflare for Cloudflare's edges plus the default). A list replaces the default; see Behind a reverse proxy
OPENTODOPUSHonoff disables web push for reminders: no requests to push services; reminders still show in the app
OPENTODOWEBHOOKSALLOWPRIVATEunsettrue/false presets the owner's "allow private network targets" webhook policy on first start; afterwards Settings decides
OPENTODOMCPunset (off)true presets the AI assistants (MCP) endpoint on first start; afterwards Settings → Integrations → AI assistants decides
OPENTODOMCPWRITESunset (off)true presets "Assistants may make changes" on first start; each user must still allow it and use an agent token. Afterwards Settings → Integrations → AI assistants decides
OPENTODOCALDAVunset (off)true presets the CalDAV endpoint on first start; afterwards Settings → Integrations → Calendar apps decides
OPENTODOOIDCISSUER, CLIENTID, CLIENTSECRET, DISPLAYNAME, SSOONLYunsetOptional single sign-on settings; each one set overrides and locks that field in Settings → Access policies → Single sign-on. The issuer must be https
OPENTODOSMTPHOST, SECURITY, USER, PASSWORD (or PASSWORDFILE), FROMunset (off)Optional outgoing email for password resets, sign-up confirmations and invitations; HOST and FROM together; each one set overrides and locks that field in Settings → Server → Outgoing email
OPENTODOSTTURL, MODEL, KEYFILEunset (off)Optional speech-to-text service for dictation (OpenAI-compatible /v1/audio/transcriptions); a set variable makes its field read-only in Settings
OPENTODOEMAILADDRESS, IMAPHOST, IMAPUSER, IMAPPASSWORD (or IMAPPASSWORDFILE)unset (off)Email to task: all four together turn it on; setting only some stops startup
OPENTODOEMAILIMAPMAILBOX, POLLSECONDS, PROCESSEDINBOX, 60, move:OpenTodo/ProcessedEmail to task tuning (minimum poll 15 s; PROCESSED may be delete)
OPENTODOBACKUPunsetOptional presets for scheduled backups (DESTINATION, SCHEDULE, TIME, WEEKDAY, KEEP, MAXAGEDAYS, COMPRESS, LOCALDIR, S3, WEBDAV, PASSPHRASE/PASSPHRASEFILE), applied once; Settings → Backups wins afterwards
OPENTODOHOSTURL, TOKEN, OPERATORADDR, IDLEMINUTES, MAXOPEN, LIMITSunset (off)Host mode for operators of a hosted service: many tenants in one process, chosen by host name. Not needed to self-host

OPENTODO_PORT and OPENTODO_BIND in .env only change the host port and address published by docker-compose.yml (default 0.0.0.0:8080, every interface; behind a reverse proxy on the same machine, OPENTODO_BIND=127.0.0.1 is the safer choice). The ot client reads its own variables (OPENTODO_URL, OPENTODO_TOKEN, OPENTODO_PROFILE, OPENTODO_CONFIG), see docs/cli.md.

Adding people

Sign-up stays closed after the owner account exists. To add a partner or a colleague, open Settings → Administration → Members, optionally enter their email, pick an expiry (1 hour to 30 days, default 7 days) and create an invitation. Copy the link (it is shown once) and send it however you like; OpenTodo sends no email. The link creates exactly one member account and then stops working; you can revoke a pending link at any time. Admins can create invitations too.
Manage accounts afterwards in Settings → Administration → Users: promote admins, deactivate and reactivate, issue password reset links, delete accounts, transfer ownership (docs/self-hosting/docker.md). If the owner forgets their password, opentodo reset-owner-password on the host prints a reset link. With single sign-on configured, people can also sign in through your identity provider. Set OPENTODO_BASE_URL so links point at your public URL. API details: docs/api.md.

How it works

┌───────────────────────── device ─────────────────────────┐      ┌──────────── server ────────────┐
│ React PWA  ──►  IndexedDB (source of truth)              │      │ Go binary + SQLite             │
│                 │ every edit = field ops {ts, device}    │ sync │  ops log (append-only)         │
│                 └► outbox ──────────────────────────────►├─────►│  fields (last-writer-wins)     │
│                 ◄─ ops from other devices ◄──────────────┤◄─────┤  REST, CalDAV, email, import   │
└──────────────────────────────────────────────────────────┘      │  all write through ops         │
                                                                  └────────────────────────────────┘

  • Every change is a small operation that sets one field of one entity, stamped with a hybrid logical clock and the device id.
  • Clients apply ops locally first, then push the outbox and pull what other devices wrote. The same per-field last-writer-wins rule runs on every replica, so all devices converge no matter the order ops arrive in. Rich-text notes are mergeable documents, so concurrent edits to one note combine instead of replacing each other.
  • Each op belongs to a sharing scope (your personal data, or one shared project); the server checks the author's role when it receives an op.
  • Connected devices keep a lightweight server-sent event stream open; when the server records new ops it tells the other devices "sync now" (no data on the wire). If the stream is unavailable the app polls.
  • The REST API, CalDAV, email capture and imports write through the same log, so a task created with curl shows up on your phone right away.
  • Deletes are tombstones, which keeps sync correct and powers the Trash.

More in docs/architecture.md and docs/sync.md.

Documentation

Read the docs with screenshots and search at open-todo.app/docs, or in the app under Help (offline, matching your server's version). The same pages are Markdown in docs/:

Development

make dev        # Go API on :8080 + Vite with hot reload on :5173
make check      # vet, typecheck, Go + web + extension tests, docs check, OpenSpec validation (what CI runs)
make docs       # regenerate the docs website and in-app help from docs/

Repository layout:
cmd/opentodo/        server entry point (serve, restore, mfa-reset, reset-owner-password, healthcheck, version)
cmd/ot/              command-line client entry point (logic in internal/cli)
internal/            Go packages: store, sync, auth, httpapi, caldav, mcp, email, push, backup, … (see docs/architecture.md)
web/                 React 19 + Vite + Tailwind PWA; builds into internal/web/dist
extension/           browser capture extension (Manifest V3, plain JS)
openspec/            specs (live behaviour contracts) and changes (proposals in flight and archived)
docs/                user, operator and protocol documentation

Working with specs

This repository uses OpenSpec. The current behaviour of the product is described in openspec/specs/<capability>/spec.md; each new feature is a change folder with a proposal, delta specs, a design and a task list. With Claude Code or any supported assistant:
/opsx:propose "natural language quick add"   # plan a change
/opsx:apply                                  # implement it
/opsx:archive                                # merge its specs into the live ones

Validate everything with make openspec-validate (the OpenSpec CLI at the version pinned in the Makefile; also part of make check and CI).

Privacy

The web app makes no network requests except to your own server: no analytics, crash reporters, update pings or third-party fonts. The server makes outbound requests only for features you configure:
  • web push to the push service a device's browser chose (Google, Mozilla, Apple), end-to-end encrypted and carrying only ids unless the device opts in to titles (OPENTODO_PUSH=off turns push off);
  • webhook URLs you add;
  • backup destinations (S3 or WebDAV) you configure;
  • the single sign-on issuer you configure;
  • the IMAP mailbox you configure for email to task;
  • the SMTP server you configure for outgoing email;
  • the GitHub or GitLab host people connect for issue import, with their token sent only to that host (private network addresses only if you allow them);
  • the calendar addresses people add to show other calendars beside their tasks (private network addresses only if you allow them);
  • the speech-to-text service you connect for dictation, only while someone who chose it dictates a phrase;
  • the model provider you enable for the in-app assistant (off by default), only after a person has accepted what is sent and only while they wait for an answer.

The CalDAV and MCP endpoints only answer requests; they never call out. The audit log records security events (sign-ins, tokens, invitations, exports), never task content, and stores client addresses only if the owner turns that on; see SECURITY.md. With optional end-to-end encryption the server holds only ciphertext for task content.

Contributing

Issues and pull requests are welcome, and so are questions on Discord. Please read CONTRIBUTING.md: changes that alter behaviour go through an OpenSpec change first. Security issues go to the address in SECURITY.md.

License

AGPL-3.0-only. You can run, study, modify and share OpenTodo freely. If you offer a modified version as a network service, you must offer its source under the same license. That is what keeps a self-hosted project from being quietly turned back into a subscription.

Serve OpenTodo on your own domain behind Caddy, Nginx or Traefik. Fill in your domain and copy the result. It's a starting point, some apps need their own base URL or extra headers set too.

Proxying opentodo.example.com to http://opentodo:8080

Add this to your Caddyfile

opentodo.example.com {
	reverse_proxy http://opentodo:8080
}

Check the logs first

Nine times out of ten the logs tell you exactly what went wrong.

  • In Portainer, go to Containers, click the container, then Logs. Or run docker logs opentodo
  • Exit codes help too: 137 means killed, usually out of memory. 126 or 127 means the command inside the image is broken.

Port already in use

If deployment fails with "Bind for 0.0.0.0:8080 failed: port is already allocated", something else on your server is using that port.

  • Find what's using it: sudo ss -tlnp | grep :8080
  • Stop the other service, or pick a different host port. In 8080:8080 only the left number is yours to change, the right one belongs to the app.

Running but the page won't load

The container is up but nothing appears in your browser.

  • Use your server's real IP: http://your-server-ip:8080. The 0.0.0.0 link Portainer shows isn't a real address.
  • Give it a minute after first deploy, opentodo can take a while to initialise.
  • Make sure your firewall allows the port, e.g. sudo ufw allow 8080

Image won't pull

Test the pull directly on the host: docker pull ghcr.io/pixari/opentodo:stable

  • "manifest unknown" means the tag no longer exists.
  • "toomanyrequests" is the Docker Hub rate limit. Log in with docker login to raise it.
  • "no space left on device" means a full disk. Reclaim space with docker system prune

"exec format error"

This means the image was built for a different CPU architecture than your server.

  • This image supports: amd64, arm64
  • Check yours with uname -m: x86_64 is amd64, aarch64 is arm64. Raspberry Pi and other ARM boards are the usual culprits.

Container keeps restarting

The unless-stopped restart policy relaunches the app after every crash, so the real error can scroll past.

  • Check the logs right after a restart, the last few lines before it died are the useful ones.
  • Get the exit code with docker inspect opentodo --format '{{.State.ExitCode}}'
  • Still stuck? Redeploy once with the restart policy set to no so the failure stays visible.

Required settings are blank

OPENTODO_SETUP_TOKEN, OPENTODO_BASE_URL, OPENTODO_TRUSTED_PROXIES have no default value, and opentodo may crash or misbehave if left empty.

  • Fill them in on the deploy screen before hitting deploy.

Raise an issue

Found something which isn't working as it should? Here's how to report it.

A single container

OpenTodo runs as one container, the simplest kind of app here. Just the one image to pull and nothing else wired up alongside it.

The app image

An image is the app packed up ready to go, everything OpenTodo needs bundled into one download. This template pulls ghcr.io/pixari/opentodo:stable, which Docker fetches once (about 13 MB) and then starts your own copy from.

Where the image comes from

Docker pulls its images from registries, public libraries of ready-built apps. OpenTodo's comes from the GitHub Container Registry, published by pixari.

Version tags

The bit after the colon in the image name is the version tag. This one pins stable, so every redeploy gives you that exact build until you bump it yourself.

Which machines it runs on

Every image is built for particular CPU types. This one ships for amd64, arm64, so it runs on both regular x86 servers and ARM boards like a Raspberry Pi.

Ports

A port is the door the app answers on. A mapping like 8080:8080 means it's reachable on port 8080 of your server, where the left number is yours to change and the right one belongs to the app. Once it's running, open http://your-server-ip:8080 in a browser. It opens:

  • 8080:8080, likely the web interface

Volumes

A volume is where OpenTodo keeps its files so they survive an update or a restart. Without one, anything it saves would sit inside the container and vanish the moment it's recreated. This template mounts:

  • /data as a volume Docker manages for you

Environment variables

Environment variables are the settings you hand over when you deploy, things like a password or a timezone. OpenTodo takes 4 of them, and 3 need a value before it'll start properly:

  • OPENTODO_SETUP_TOKEN, needs a value. When set, creating the first (owner) account asks for this token, so a server others can reach cannot be claimed by a stranger. Generate one with: openssl rand -hex 16
  • OPENTODO_BASE_URL, needs a value. The https address people open behind a reverse proxy, for example https://todo.example.com. Leave empty when you open OpenTodo by the server's address over plain http: an https value there breaks sign-in.
  • OPENTODO_ALLOW_SIGNUP, pick one of false, true. True lets anyone who can reach the server create an account. Otherwise the owner invites people.
  • OPENTODO_TRUSTED_PROXIES, needs a value. Reverse proxies whose X-Forwarded-For is trusted for the visitor's address: IP addresses or ranges such as 192.0.2.0/24 separated by commas, or cloudflare. Set it only when every request comes through them.

Restart policy

The restart policy here is unless-stopped, so Docker restarts OpenTodo after a crash or reboot, but leaves it off when you stop it on purpose. You can change this on the deploy screen. The choices are no (never restart), on-failure (only after a crash), unless-stopped (restart unless you stop it), and always (bring it back no matter what).

Networking

Nothing custom is set, so OpenTodo sits on Docker's default bridge network: its own private space that reaches the outside world only through the ports it publishes.

Container name

Once it's deployed, Portainer names the container opentodo. That's what you'll spot in the containers list and use in commands like docker logs opentodo.

Platform

The platform is linux, the kind of system the container is built to run on. Docker and Portainer handle this on a normal Linux server.

Open source license

OpenTodo is open source, released under the AGPL-3.0 license. In plain terms the code is out in the open, so you're free to run it and change it to fit what you need.

Portainer app templates

Zooming out, this whole page comes from a Portainer app template: a short recipe telling Portainer how to set OpenTodo up. Add the template list to Portainer once, then deploying OpenTodo is a click rather than a wall of config.