OS Task Management: Tasktrove and Tududi

Published: Sep 11, 2025 by Isaac Johnson

Marius has brought up a couple of good Open-Source project/task management tools lately. First is a very simple clean board, TaskTrove which we will explore in docker and Kubernetes. We’ll also touch on some of their planned updates.

The other tool we’ll check out is Tududi which has a lot more features. Again, we’ll check it out in docker then Kubernetes and then look into the API endpoints and how we might integrate with them.

Let’s start with Tasktrove…

TaskTrove

A few weeks back I saw this Marius post about TaskTrove by “Doh Simpson”.

We can fire it up locally with:

$ mkdir ./data && chmod 777 ./data
$ docker run -p 3333:3000 -v ./data:/app/data -d --name tasktrove ghcr.io/dohsimpson/tasktrove
Unable to find image 'ghcr.io/dohsimpson/tasktrove:latest' locally
latest: Pulling from dohsimpson/tasktrove
9824c27679d3: Pull complete
6a088b2daae0: Pull complete
52719e552fdf: Pull complete
016c0e952111: Pull complete
c18378b3f025: Pull complete
d78be5ab4256: Pull complete
891fdabdefbc: Pull complete
1fa01ae92314: Pull complete
4daf10782d6b: Pull complete
0bee24fac04a: Pull complete
Digest: sha256:e58c67de4e81510a877fa6f368f04b580ebd852b96ffd56ab093caf7a6345da7
Status: Downloaded newer image for ghcr.io/dohsimpson/tasktrove:latest
f9e053487300865296cf0184634f13c645263bae9ef05cf1d4b7c58f8f37aa2f

We can now initialize our instance

/content/images/2025/09/tasktrove-01.png

One of the settings we can enable are browser notifications

/content/images/2025/09/tasktrove-02.png

We can then test notifications (and in my case, I heard an audible tone)

/content/images/2025/09/tasktrove-03.png

Adding Projects

First, let’s add a project

/content/images/2025/09/tasktrove-04.png

Next we can add a task

/content/images/2025/09/tasktrove-05.png

I didn’t notice the “What needs to be done” was actually a subject line. Once I set it, then it would let me create

/content/images/2025/09/tasktrove-06.png

By default, I’m looking at all tasks on all boards

/content/images/2025/09/tasktrove-07.png

But if I select my project, then it just narrows it down to tasks in that project

/content/images/2025/09/tasktrove-08.png

I can also filter it, for instance, just to tasks of P2. Since I just have a single P1 right now, that list is empty

/content/images/2025/09/tasktrove-09.png

Tasks can have subtasks. The UI was not obvious to me, but I saw we could add them from the small check icon

/content/images/2025/09/tasktrove-10.png

Kubernetes

Docker is fine, but let’s launch this into K8s using a YAML manifest

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: tasktrove-data-pvc
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: local-path
  resources:
    requests:
      storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tasktrove
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tasktrove
  template:
    metadata:
      labels:
        app: tasktrove
    spec:
      containers:
        - name: tasktrove
          image: ghcr.io/dohsimpson/tasktrove:latest
          ports:
            - containerPort: 3000
          volumeMounts:
            - name: data
              mountPath: /app/data
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: tasktrove-data-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: tasktrove
spec:
  type: ClusterIP
  selector:
    app: tasktrove
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000

We can now apply

$ kubectl apply -f ./tasktrove.yaml
persistentvolumeclaim/tasktrove-data-pvc created
deployment.apps/tasktrove created
service/tasktrove created

When I see the pod is up

$ kubectl get po -l app=tasktrove
NAME                         READY   STATUS    RESTARTS   AGE
tasktrove-5bdccdfcfc-5mkr8   1/1     Running   0          5m1s

Let’s port-forward to access it

$ kubectl port-forward tasktrove-5bdccdfcfc-5mkr8 8888:3000
Forwarding from 127.0.0.1:8888 -> 3000
Forwarding from [::1]:8888 -> 3000

/content/images/2025/09/tasktrove-11.png

we can add a task

/content/images/2025/09/tasktrove-12.png

Let’s fire up an A Record

$  az account set --subscription "Pay-As-You-Go" && az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 75.73.224.240 -n tasktrove
{
  "ARecords": [
    {
      "ipv4Address": "75.73.224.240"
    }
  ],
  "TTL": 3600,
  "etag": "071c8bc6-a69b-40dd-b083-14aa47810215",
  "fqdn": "tasktrove.tpk.pw.",
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/tasktrove",
  "name": "tasktrove",
  "provisioningState": "Succeeded",
  "resourceGroup": "idjdnsrg",
  "targetResource": {},
  "trafficManagementProfile": {},
  "type": "Microsoft.Network/dnszones/A"
}

I can then add an Ingress

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: azuredns-tpkpw
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.org/client-max-body-size: "0"
    nginx.org/proxy-connect-timeout: "3600"
    nginx.org/proxy-read-timeout: "3600"
    nginx.org/websocket-services: tasktrove
  name: tasktrove-ingress
spec:
  rules:
  - host: tasktrove.tpk.pw
    http:
      paths:
      - backend:
          service:
            name: tasktrove
            port:
              number: 80
        path: /
        pathType: Prefix
  tls:
  - hosts:
    - tasktrove.tpk.pw
    secretName: tasktrove-tls

I can now apply

$ kubectl apply -f tasktrove.yaml
persistentvolumeclaim/tasktrove-data-pvc unchanged
deployment.apps/tasktrove unchanged
service/tasktrove unchanged
Warning: annotation "kubernetes.io/ingress.class" is deprecated, please use 'spec.ingressClassName' instead
ingress.networking.k8s.io/tasktrove-ingress created

When I see the cert is satisified

$ kubectl get cert tasktrove-tls
NAME            READY   SECRET          AGE
tasktrove-tls   True    tasktrove-tls   2m49s

We can see serves up our Task app

/content/images/2025/09/tasktrove-13.png

However, the current version has no user delineation, so anyone viewing the page is admin

/content/images/2025/09/tasktrove-14.png

One thing you’ll note is there is an “Upgrade to Pro” button under the user dropdown

/content/images/2025/09/tasktrove-15.png

Which has a Pro version (Coming Soon) that would include multi-user support

/content/images/2025/09/tasktrove-16.png

I did sign up for notifications so when it goes live, I’ll circle back.

Tududi

Another one from Marius is Tududi (huhuhuh… he said doody…)

Let’s start with docker and go from there

$ mkdir db
$ mkdir uploads

# determine my userid and group id
$ id -u builder
1000
$ id -g
1000

$ docker run \
  -e TUDUDI_USER_EMAIL=isaac@freshbrewed.science \
  -e TUDUDI_USER_PASSWORD=notmyrealpassword \
  -e TUDUDI_SESSION_SECRET=$(openssl rand -hex 64) \
  -e TUDUDI_ALLOWED_ORIGINS=https://localhost,http://localhost:3002,https://127.0.0.1,http://127.0.0.1:3002 \
  -e PUID=1000 \
  -e GUID=1000 \
  -v /home/builder/Workspaces/tududi/db:/app/backend/db \
  -v/home/builder/Workspaces/tududi/uploads:/app/backend/uploads \
  -p 3002:3002 \
  -d chrisvel/tududi:latest

I can now login

/content/images/2025/09/tududi-01.png

And I’m presented with a nice daily plan - I really like this as this is usually how I think (what am I doing and what did I just do)

/content/images/2025/09/tududi-02.png

Let’s create our first project and task:

The only annoyance I have is that the timeline does not have a proper scroll so I had to expand my window to see it all

/content/images/2025/09/tududi-04.png

In exploring I found it has a built-in pomodoro clock (I like this!) which shows when you click the tomato icon in the upper right (Note: Pomodoro is a time management technique named after the Italian 25-minute timers shaped like tomatoes)

/content/images/2025/09/tududi-05.png

There is a note an inbox for quick capturing of thoughts

K8s

Instead of just converting this to a Kubernetes YAML, I’ll instead look to run this on the Dockerhost and expose as an endpoint

builder@builder-T100:~/tududi$ pwd
/home/builder/tududi
builder@builder-T100:~/tududi$ ls
db  uploads
builder@builder-T100:~/tududi$ id -u builder && id -g
1000
1000
builder@builder-T100:~/tududi$ docker run \
  -e TUDUDI_USER_EMAIL=isaac@freshbrewed.science \
  -e TUDUDI_USER_PASSWORD='notmyrealpassword' \
  -e TUDUDI_SESSION_SECRET=$(openssl rand -hex 64) \
  -e TUDUDI_ALLOWED_ORIGINS=https://localhost,http://192.168.1.100:3002,http://127.0.0.1:3002,https://tududi.tpk.pw, \
  -e PUID=1000 \
  -e GUID=1000 \
  -v /home/builder/tududi/db:/app/backend/db \
  -v/home/builder/tududi/uploads:/app/backend/uploads \
  -p 3002:3002 \
  -d chrisvel/tududi:latest
Unable to find image 'chrisvel/tududi:latest' locally
latest: Pulling from chrisvel/tududi
9824c27679d3: Already exists
8c59d92d6fc9: Pull complete
54225bd60196: Pull complete
a9e48ad1219d: Pull complete
fe98319300d8: Pull complete
dd632f3e5af3: Pull complete
a04f39f71be5: Pull complete
430b2b48a45d: Pull complete
4f4fb700ef54: Pull complete
0eb3ac27a284: Pull complete
c9eb2dd5b9f2: Pull complete
70de60960b5e: Pull complete
3716a18ce972: Pull complete
93ed3f3c8434: Pull complete
9d3f13871774: Pull complete
1b40a209e191: Pull complete
aee3e3f9aac9: Pull complete
e0737eb1a88d: Pull complete
Digest: sha256:441fb45d3d72a296738f663cb77546840db02df31ea3a4f656a5ee808a06be2e
Status: Downloaded newer image for chrisvel/tududi:latest
6882d23500dacbf7377db85c0611d163b41b24e853f057f75532d6382688f34b

To expose this, I need an A Record

$ az account set --subscription "Pay-As-You-Go" && az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 75.73.224.240 -n tududi
{
  "ARecords": [
    {
      "ipv4Address": "75.73.224.240"
    }
  ],
  "TTL": 3600,
  "etag": "9aabc0d3-9c25-49e5-a23e-fb891589ec58",
  "fqdn": "tududi.tpk.pw.",
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/tududi",
  "name": "tududi",
  "provisioningState": "Succeeded",
  "resourceGroup": "idjdnsrg",
  "targetResource": {},
  "trafficManagementProfile": {},
  "type": "Microsoft.Network/dnszones/A"
}

Then I can just create an Ingress, Service and Endpoint to forward traffic

$ cat ./tududi.yaml
apiVersion: v1
kind: Endpoints
metadata:
  name: tududi-external-ip
subsets:
- addresses:
  - ip: 192.168.1.100
  ports:
  - name: tududiint
    port: 3002
    protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: tududi-external-ip
spec:
  clusterIP: None
  clusterIPs:
  - None
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
  - IPv6
  ipFamilyPolicy: RequireDualStack
  ports:
  - name: tududi
    port: 80
    protocol: TCP
    targetPort: 3002
  sessionAffinity: None
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: azuredns-tpkpw
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.org/websocket-services: tududi-external-ip
  generation: 1
  name: tududiingress
spec:
  rules:
  - host: tududi.tpk.pw
    http:
      paths:
      - backend:
          service:
            name: tududi-external-ip
            port:
              number: 80
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - tududi.tpk.pw
    secretName: tududi-tls

$ kubectl apply -f ./tududi.yaml
endpoints/tududi-external-ip created
service/tududi-external-ip created
Warning: annotation "kubernetes.io/ingress.class" is deprecated, please use 'spec.ingressClassName' instead
ingress.networking.k8s.io/tududiingress created

When I see the cert satisified

$ kubectl get cert tududi-tls
NAME         READY   SECRET       AGE
tududi-tls   False   tududi-tls   114s
$ kubectl get cert tududi-tls
NAME         READY   SECRET       AGE
tududi-tls   True    tududi-tls   21m

I can now access externally

/content/images/2025/09/tududi-07.png

One of my first changes was to set my timezone

/content/images/2025/09/tududi-08.png

Telegram Bot

Another nice feature is that Tududi can engage with us on Telegram

You can follow their readme for steps which basically involves a convo with @botfather.

But when done, we can put our bot token in the app

/content/images/2025/09/tududi-09.png

I found I needed to engage with my bot at least once in the Telegram app for the bottom “Send test summary” to work.

/content/images/2025/09/tududi-10.png

We can see the results in the Telegram app:

/content/images/2025/09/tududi-11.jpg

We also know it’s working as we see a green “connected and live” telegram icon in the lower left

/content/images/2025/09/tududi-12.png

Adding Users

While there is no user management (yet) in the UI, nor the ability to create from the login page, there is a way to add users using npm

We find the container name locally

builder@builder-T100:~/tududi$ docker ps | grep tududi
6882d23500da   chrisvel/tududi:latest                                           "/app/scripts/docker…"   48 minutes ago   Up 48 minutes (healthy)   0.0.0.0:3002->3002/tcp, :::3002->3002/tcp                                          clever_murdock
builder@builder-T100:~/tududi$ docker exec -it clever_murdock /bin/bash

Then launch a shell into to add the user

builder@builder-T100:~/tududi$ docker exec -it clever_murdock /bin/sh
/app/backend # npm run user:create builder@freshbrewed.science 2Legit2quit\!

> tududi@v0.80 user:create
> cd backend && node scripts/user-create.js builder@freshbrewed.science asdfasdfasdfasdf

Using database file 'db/production.sqlite3'
Creating user with email: builder@freshbrewed.science
✅ User created successfully
📧 Email: builder@freshbrewed.science
🆔 User ID: 2
📅 Created: Wed Sep 10 2025 12:16:24 GMT+0000 (Coordinated Universal Time)
npm notice
npm notice New major version of npm available! 10.8.2 -> 11.6.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v11.6.0
npm notice To update run: npm install -g npm@11.6.0
npm notice
/app/backend #

I can now login as the new builder user

/content/images/2025/09/tududi-13.png

The comments in the code note that you can use the same script to update the password as well.

While there is not a way to delete a user, an admin could always go and change the password to something long and unshared to lock them out.

API access

There are some APIs I can see in the code.

Certainly, an easy one to see is the health check (which would be used if you made this into a Kubernetes replicaset)

/content/images/2025/09/tududi-14.png

I haven’t sorted out the auth flow on the command line, but I can see that if I could, I could get back JSON blocks of the output

/content/images/2025/09/tududi-15.png

I tried basic auth tokens, and basic auth login. I finally hit Gemini to see if it could solve it

/content/images/2025/09/tududi-16.png

While it suggested

  I will execute the following curl command to log you in. I'll use a placeholder for the port (3000), which is a
  common default for Node.js applications.

   1 curl -X POST \
   2   -H "Content-Type: application/json" \
   3   -d '{"email": "user@example.com", "password": "MyPassword"}' \
   4   -c cookies.txt \
   5   http://localhost:3000/api/login

  Step 2: Fetch tasks using the session cookie

  Now, you can make a GET request to the /api/tasks endpoint. The -b flag sends the cookie that was saved in
  cookies.txt, authenticating your request.

   1 curl -b cookies.txt http://localhost:3000/api/tasks

  This will return a JSON object containing your tasks. Let me know if you'd like me to execute the login command for
  you.

It did not work - the login returned, but did not get me a session token or cookie

builder@DESKTOP-QADGF36:~/Workspaces/tasktrove$ curl -X GET -H "Content-Type: application/json" -d '{"email": "isaac@freshbrewed.science", "password": "xxxxxxxxxxxxxxx"}' -c cookies.txt https://tududi.tpk.pw/api/tasks
builder@DESKTOP-QADGF36:~/Workspaces/tasktrove$ curl -X POST -H "Content-Type: application/json" -d '{"email": "isaac@freshbrewed.science", "password": "xxxxxxxxxxxxxxx"}' -c cookies.txt https://tududi.tpk.pw/api/login
{"user":{"id":1,"email":"isaac@freshbrewed.science","language":"en","appearance":"light","timezone":"America/Chicago"}}


builder@DESKTOP-QADGF36:~/Workspaces/tasktrove$ curl -X GET -c cookies.txt https://tududi.tpk.pw/api/tasks
{"error":"Authentication required"}

builder@DESKTOP-QADGF36:~/Workspaces/tasktrove$ cat cookies.txt
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

However, I came back later with Gemini and tried again - this time the command (subtley different - uses cookie-jar not cookies) worked

$ curl -X POST \
-H "Content-Type: application/json" \
-d '{"email": "isaac@freshbrewed.science", "password": "xxxxxxxxxxxxxxxxxxxxxxxxx"}' \
--cookie-jar cookies.txt \
https://tududi.tpk.pw/api/login

$ curl -X GET \
--cookie cookies.txt \
https://tududi.tpk.pw/api/tasks

For instance

$ curl -X GET --cookie cookies.txt https://tududi.tpk.pw/api/tasks | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   575  100   575    0     0   1979      0 --:--:-- --:--:-- --:--:--  1975
{
  "tasks": [],
  "metrics": {
    "total_open_tasks": 0,
    "tasks_pending_over_month": 0,
    "tasks_in_progress_count": 0,
    "tasks_in_progress": [],
    "tasks_due_today": [],
    "today_plan_tasks": [],
    "suggested_tasks": [],
    "tasks_completed_today": [],
    "weekly_completions": [
      {
        "date": "2025-09-04",
        "count": 0,
        "dayName": "Thu"
      },
      {
        "date": "2025-09-05",
        "count": 0,
        "dayName": "Fri"
      },
      {
        "date": "2025-09-06",
        "count": 0,
        "dayName": "Sat"
      },
      {
        "date": "2025-09-07",
        "count": 0,
        "dayName": "Sun"
      },
      {
        "date": "2025-09-08",
        "count": 0,
        "dayName": "Mon"
      },
      {
        "date": "2025-09-09",
        "count": 0,
        "dayName": "Tue"
      },
      {
        "date": "2025-09-10",
        "count": 0,
        "dayName": "Wed"
      }
    ]
  }
}

Let’s try adding a high priority task next

$ curl -X POST \
   -H "Content-Type: application/json" \
   --cookie cookies.txt \
   -d '{
     "name": "Deploy the new feature",
     "priority": "high",
     "due_date": "2025-09-15"
   }' \
   https://tududi.tpk.pw/api/task
{"id":1,"uuid":"2006e657-6317-434f-919d-d799116f960b","name":"Deploy the new feature","description":null,"due_date":"2025-09-15","today":false,"priority":2,"status":0,"note":null,"recurrence_type":"none","recurrence_interval":null,"recurrence_end_date":null,"last_generated_date":null,"recurrence_weekday":null,"recurrence_month_day":null,"recurrence_week_of_month":null,"completion_based":false,"user_id":1,"project_id":null,"recurring_parent_id":null,"parent_task_id":null,"completed_at":null,"created_at":"2025-09-11T00:25:15.939Z","updated_at":"2025-09-11T00:25:15.939Z","Tags":[],"Project":null,"tags":[]}

I can now see it on the board

/content/images/2025/09/tududi-18.png

with the expected details

/content/images/2025/09/tududi-17.png

Let’s pack a note in there

$ curl -X POST    -H "Content-Type: application/json"    --cookie cookies.txt    -d '{
     "name": "Some other feature",
     "priority": "medium",
     "note": "stuff and stuff\nand more things\n\nplease"}'    https://tududi.tpk.pw/api/task

While markdown is not supported, we can see new lines work just fine

/content/images/2025/09/tududi-19.png

Summary

We took a look at two nice task management tools; TaskTrove and Tududi.

Tasktrove has promise. I like the UI and it is quite smooth. While it lacks user management right now (so it is just wide open if exposed), I do like how it makes it easy to export (and reimport) your data

/content/images/2025/09/tasktrove-17.png

If running in Docker, you can schedule or trigger backups as well

/content/images/2025/09/tasktrove-18.png

However, without being able to prompt for a login, I don’t see myself using it in it’s current form.

Tududi has a lot more promise for me. It has user support out of the box, even if management requires me to shell into the container. The SQLite database is in the PVC if in k8s or in a local docker volume if not so I can back that up myself.

While the REST API was not well documented (could be a good feature to add OpenAPI spec), it was easy enough to discover and use. This means I could integrate it into my flow.

Additionally, the telegram bot support is interesting (but I’m not sure I would use it all that much). Here is what it looked like the next day with the daily summary:

/content/images/2025/09/tududi-20.jpg

I plan to keep trying out Tududi if for no other reason that it has “doody” in the name and I’m immature at heart…

tududi tasktrove task opensource docker kubernetes

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes