Vikunja and the Self-Hosted Task-List Trap
A good todo app, and the reason your last four todo apps failed

Contents
I have migrated my task list eleven times. Remember the Milk, Wunderlist, Todoist twice, Trello, Notion, org-mode for a heroic and exhausting four months, a plain text file, Microsoft To-Do because work made me, Taskwarrior, a plain text file again, and now Vikunja. Eleven migrations in about fifteen years, which works out at a fresh start every sixteen months.
Each migration followed an identical script. Week one: euphoria, everything imported, elegant project hierarchy, labels colour-coded, saved filters that are frankly a work of art. Week three: it is working, this is the one. Week eight: I have stopped opening it, and there is now a Post-it on my monitor. Week ten: the Post-it has a Post-it.
Eleven data points is enough to notice the pattern, and the pattern indicts me rather than the software. This post is about Vikunja, which is genuinely good and which I have now used for over a year — the longest any system has survived. It is also about why the previous ten died, because if you skip that part you will simply run this container for eight weeks and then start looking at the next one.
The trap, stated plainly
A self-hosted task list has a property that Todoist does not: you can change it. Schema, fields, views, filters, the lot. And that turns out to be corrosive, because tuning the system feels exactly like doing the work. Same desk, same keyboard, same pleasant sense of forward motion, zero output.
I have spent an entire Sunday building a saved filter that surfaced “tasks due this week, in projects tagged @home, excluding anything blocked, sorted by priority then due date”. It was a beautiful filter. The tasks it surfaced were: bleed the radiators, ring the dentist. I knew both of those. I had known them for a month. The filter changed nothing except that I had now also spent a Sunday.
That is the trap. The homelab version of the same disease shows up everywhere — I wrote about its general form in The Home Lab Upgrade Trap: When “Good Enough” Should Be Good Enough — and a task manager is the purest expression of it, because the tool’s entire subject matter is productivity. You can procrastinate inside it and the tool will congratulate you.
So the only defensible reason to run a self-hosted task list is that you want the data on your own disk and you accept the tool will be about as good as the commercial one. Any pitch that starts “and I can customise it to fit my exact workflow” is the trap talking. Your exact workflow does not exist. You have three projects and you keep forgetting the dentist.
With that established: Vikunja is a good task list.
What Vikunja is
A Go backend and a Vue frontend, written by Konrad Langenberg, open source under AGPL. It runs as a single binary with SQLite and no external dependencies, which is an increasingly rare and admirable thing. Since 0.22 the API and frontend ship in one container image, which removed the most confusing part of the old setup (two containers plus an nginx to glue them, a topology that generated most of the project’s support traffic).
What it gives you:
- Projects with nesting, each viewable as List, Gantt, Table or Kanban. Same data, four views, switchable per project.
- Quick Add Magic: type
Ring the dentist tomorrow at 10 *health !3 +Adminand it parses the date, the label, the priority and the project out of the sentence. This is the single feature most likely to keep you using it, because the cost of capture is the whole ballgame. - CalDAV, so tasks show up in Apple Reminders, Thunderbird, or DAVx5 on Android. Read and write.
- A real REST API with OpenAPI docs, which means scripting is trivial.
- Saved filters, labels, assignees, teams, and reminders that can hit email.
- Migrators from Todoist, Trello, Microsoft To-Do and Vikunja’s own export format.
What it lacks, honestly: a first-class native mobile app. There is a community Flutter app and a very good PWA, and CalDAV covers the “add task from phone” case. If a polished iOS app is your requirement, stop here and pay Todoist.
The compose file
| |
Three things in there earn their keep. ENABLEREGISTRATION=false after you have made your account, because an open registration endpoint on a public URL is an invitation. The condition: service_healthy dependency, because Vikunja runs migrations at start and a Postgres that has not finished initialising produces a confusing crash loop — the general pattern is in Docker Compose Patterns That Age Well. And PUBLICURL, which must exactly match the URL you browse to, including scheme and any trailing path.
Honestly, if you are one person: use SQLite. Drop the whole vikunja-db service and the four VIKUNJA_DATABASE_* lines and Vikunja will put a file in /app/vikunja/files. It will be faster, the backup is one file, and there is no scenario where a personal todo list outgrows it. I run Postgres because the instance was already there for other things, which is the only good reason.
The thing you must get right first: PUBLICURL
More Vikunja support threads are about this one setting than everything else combined. The frontend, served from the same container, needs to know the absolute URL of the API. Get it wrong and you see a login page that looks perfect and rejects every attempt with a network error in the console.
Behind a reverse proxy the config must reflect the outside address:
| |
| |
If that prints something with localhost or a bare IP in it, fix VIKUNJA_SERVICE_PUBLICURL before touching anything else. Every other symptom you are chasing is probably downstream of it.
Troubleshooting the rest
Every login fails silently after a restart. The JWT secret changed. If you never set JWTSECRET, Vikunja generates a random one at start, so every restart invalidates every session and, more entertainingly, every API token. Set it explicitly from a file and never change it. Handling that secret properly rather than pasting it into the compose file is the argument I made in Secrets Management with SOPS and age.
CalDAV works in Thunderbird and fails on iOS. iOS is stricter about TLS than anything else on the network. A self-signed certificate that Thunderbird shrugs at will cause Apple Reminders to fail without a usable error. Use a real certificate. The CalDAV endpoint, for reference, is:
| |
Attachments upload and then 413. The reverse proxy’s body-size limit, sitting in front of Vikunja’s own VIKUNJA_FILES_MAXSIZE (20MB by default). Two limits, both must allow the file, and the error only mentions one of them.
Reminders never arrive. Vikunja needs a mailer configured (VIKUNJA_MAILER_*) and it silently does nothing without one. There is no warning in the UI. Check the logs at start:
| |
That line is the whole diagnosis, and you have to go looking for it.
Gantt view shows nothing. Gantt renders tasks with both a start and an end date. Tasks with only a due date are invisible in it. This is documented, unintuitive, and responsible for a lot of bug reports that are not bugs.
Backups
Small database, small files directory, no excuse.
| |
On SQLite, use sqlite3 ... ".backup" rather than copying the file, for the usual reason that a live SQLite file copied mid-write is not a database. Whatever you do, actually attempt a restore once — my standing complaint from Testing Your Backups: The Restore You Never Rehearsed applies to a todo list exactly as much as to anything else, with the small mercy that losing it is survivable.
Vikunja also has a built-in per-user data export from the settings page, which produces a zip. Worth pressing once so you know the shape of your escape hatch.
Migrating in, and the honest cost of it
Vikunja ships migrators for Todoist, Trello, Microsoft To-Do and its own export format. They live under Settings → Import, they use OAuth against the source service, and in my experience they work — with one caveat that applies to every task-manager migration ever performed.
The Todoist migrator brought across 340 tasks. Of those, roughly 300 were dead: things completed in spirit but never ticked, things that stopped mattering in 2021, and eleven separate variations on “sort out the loft”. A migrator is very good at moving your backlog and completely unable to tell you that your backlog is landfill.
So the genuinely useful migration procedure is the one that involves no migrator at all. Export the old list, read it once, and hand-type the twenty things that survive contact with the question “am I actually going to do this?”. It takes half an hour and it is the only part of the whole exercise with any value. I imported the 340 and then spent two evenings deleting them, which is how I learned this.
Scripting it, because the API is the good part
The API is where Vikunja pulls ahead of the commercial options for anyone with a homelab. Every action in the UI is a documented REST call, the OpenAPI spec is served at /api/v1/docs, and tokens are per-user and scopable from the settings page.
What that buys you in practice: your infrastructure can file its own tickets. A failing SMART check, a certificate 20 days from expiry, a backup that skipped — all of it can land in the same list as the dentist, which means one place to look.
| |
Wrap that in a shell function, call it from your monitoring hooks, and the alert that would otherwise have scrolled past in a Telegram channel becomes a thing with a due date. I pair this with dead-man’s-switch monitoring — the arrangement in Healthchecks.io Self-Hosted: Making Sure Your Cron Jobs Actually Ran — so a job that silently stops running produces a task rather than a silence.
Be disciplined about it. This is the trap wearing a hard hat: automated task creation is one afternoon away from a list containing 400 machine-generated items that you have trained yourself to scroll past. Two rules have kept mine usable. Only create a task for something a human must decide; everything a script can fix, the script fixes. And give every automated task a due date, so it either gets done or shows up as overdue and shames me into deleting it.
What actually made it stick
A year in, Vikunja has outlasted every predecessor, and I have thought about why. It has very little to do with Vikunja’s feature list.
I stopped building the system. Six projects, flat. No nesting, no Gantt, no teams, four labels. The moment I feel the urge to add a custom view, I recognise it as the trap and go bleed the radiators instead.
Capture is one keystroke. A pinned PWA tab and Quick Add Magic. If capture costs more than five seconds, the task goes on a Post-it, and the Post-it is where systems go to die.
One view, every morning. The default “Upcoming” list. That is it. The eleven previous systems all failed at the same point: the daily review became a decision about which view to open, and a decision is a place to stall.
It gets out of the way. The container has restarted for updates and nothing else. No RAM creep, no migrations that ate a Saturday, no surprise. It uses about 60MB and I forget it exists, which is the highest compliment I have for self-hosted software.
The verdict
Vikunja is a fast, well-built, genuinely pleasant task manager with a real API, honest CalDAV support, and a maintainer who has kept a coherent vision for years. As software it is a straightforward recommendation: run it, especially with SQLite, especially if you want your tasks on your own disk.
As a solution to your productivity problem it will do nothing whatsoever, and neither would Todoist, and neither did the previous ten. The system was never the bottleneck. I say this as someone who spent a Sunday on a filter that told him to ring the dentist.
Who should run it: people who already know a plain list works for them and simply want that list self-hosted, syncing to a phone, with a Kanban view for the two projects that genuinely need one.
Who should not: anyone hoping the next tool is the one that fixes it. Save the RAM. It is the Post-it, and honestly, the Post-it was fine.




