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
One of the settings we can enable are browser notifications
We can then test notifications (and in my case, I heard an audible tone)
Adding Projects
First, let’s add a project
Next we can add a task
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
By default, I’m looking at all tasks on all boards
But if I select my project, then it just narrows it down to tasks in that project
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
Tasks can have subtasks. The UI was not obvious to me, but I saw we could add them from the small check icon
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
we can add a task
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
However, the current version has no user delineation, so anyone viewing the page is admin
One thing you’ll note is there is an “Upgrade to Pro” button under the user dropdown
Which has a Pro version (Coming Soon) that would include multi-user support
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
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)
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
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)
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
One of my first changes was to set my timezone
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
I found I needed to engage with my bot at least once in the Telegram app for the bottom “Send test summary” to work.
We can see the results in the Telegram app:
We also know it’s working as we see a green “connected and live” telegram icon in the lower left
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
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)
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
I tried basic auth tokens, and basic auth login. I finally hit Gemini to see if it could solve it
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
with the expected details
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
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
If running in Docker, you can schedule or trigger backups as well
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:
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…