TaskWarrior

I’ve been looking for a better task management system to supplement my personal wiki/journal tool of silverbullet.md. Silverbullet offers some great functionality here with a bit of (space) lua, and I find it useful for managing projects, but found smaller tasks & reminders were a bit harder to implement, especially for things withspecific times or sub-tasks.

After much searching for a simple FOSS solution, I discovered TaskWarrior via a YouTube Video, which ticks all the boxes for a tool which I was looking for. I am not someone who ’lives’ in a terminal, but I don’t mind a bit of CLI and would rather that over a clunky electron (urgh!) interface. TaskWarrior (hereafter refered to as task(1)) does not come with a sync solution out-of-the-box and relies on a separate server called TaskChampion, for syncing between devices. I have days where I don’t touch my desktop or laptop, and solely rely on my phone, so having everything talking to everything else is very important. This article doesn’t seek to be a tutorial about TaskWarrior itself (that will come later), but rather the steps required to configure a TaskChampion server using podman quadlet containers.

TaskChampion

My default approach of using syncthing(1) to sync the relevant files between devices is officially recommended against by the TaskWarrior documentation, and rather preference is given to using TaskChampion to sync tasks between multiple systems. This is a tad annoying as I find syncthing the easiest solution, but after reading the (brief) documentation, I decided to give this a go.

For my setup, I’ll be using a linux desktop running the latest version of task(1), which is currently 3.4.2 at time of writing, and an Android using taskdroid which I discovered through Reddit when looking at ways to get TaskWarrior to run on my phone. For the server itself, I’ll be using a Debian 13 box using podman containers, which I prefer to docker.

Setup

Setup was relatively simple following the instructions on the docker-compose section of documentation. There was bit of translation required bewtween the docker-compose.yml file and the podman quadlet .container format. There are tools to translate these automatically, but I find it easy enough to do it manually.

Importantly the CLIENT_ID must be a VALID UUID, this can be generated on duckduckgo by typing in ‘uuid generator’. This seems to be missing from the documentation and was a small hiccup in getting everything going.

As you can have multiple different ‘users’ on a server, you can define multiple UUIDs here, one per user. As I’m going to be the sole user of this service, I’ve settled on just one UUID, which must be configured on each device that will connect to the server.

The podman quadlet .container will look something like this:

[Unit]
Description=TaskChampion Service for syncing TaskWarrior tasks
After=network-online.target

[Container]
Image=ghcr.io/gothenburgbitfactory/taskchampion-sync-server:latest
AutoUpdate=registry
ContainerName=taskchampion	
Environment=LISTEN=0.0.0.0:8080
Environment=TASKCHAMPION_SYNC_SERVER_HOSTNAME=insert your url here
Environment=CLIENT_ID=BE A VALID UUID
Network=taskchampion
PublishPort=8080:8080
Volume=/path/to/storage:/var/lib/taskchampion-sync-server/data

[Serivce]
Restart=Always
TimeoutStartSec=900

[Install]
WantedBy=default.target

As standard practice for me, I gave the container its own custom network, which is easy enough to define.

[Unit]
Description=Isolated TaskChampion Network

[Network]
NetworkName=taskchampion
Subnet=10.68.102.0/30
Gateway=10.68.102.1

[Install]
WantedBy=default.target

I then opened the relevant firewall ports on my home server, and router; and setup port fowarding. I then configured the reverse_proxy (I use Caddy) to point towards the home server setup. Taskdroid allows you to easily test the server configuration and this validated things correctly.

I then needed to insert the URL, and Client UUID into both taskdroid and .taskrc on my desktop. These obviously must match. You can then create a strong and complicated password (I use pass(1)) to input on each device too. One thing I was confused about at first, is that you do not define the secret on the server configuration, but instead on each device per UUID. That way the content is end-to-end encrypted.

Annoyingly, task(1) needs to be told to sync, via task sync for it to actually sync, and therefore I setup a simple systemd timer, but you could of course use cron or your other preferred method. I set my timer to run every 5 minutes. Helpfully, taskdroid has a button you can press, or a ‘pull-down-to-refresh’ action which is generally standard on android apps.

To test, I added a task on my phone, and then manually ran task sync on the desktop. It takes a couple of seconds to communicate with the server and pull down the latest snapshot of the task database.

Now you can have your tasks synced between all your devices! I found the setup relatively straightforward (UUID aside) and can now start using task(1) for all my task needs.