Published: Mar 5, 2024 by Isaac Johnson
ZenTao, named after the Chinese characters for “zen” and “tao,” was founded in 2009 by Attwell Wang and provides an Open-Source project management suite that is surprisingly expansive.
Today we’ll setup the Open-Source community edition using Docker and exposing with Kubernetes for TLS ingress. We’ll cover many, but not all of the features (this suite is huge!). I’ll touch on Project, Work Packages, Screens, Modules, Department and Users. We’ll look into “Spaces” and backups and wrap looking at pricing.
Company
ZenTao, founded in 2009 by ChungSheng (Attwell) Wang is based in Qingdao, China with somewhere between 50 and 1000 employees (no official details. There are 35 employees with LinkedIn profiles). The “Zen” and “Tao” are mean to “embody its roots in seeking harmony and flow within the software development process”. They are seed company with the last round in 2021.
Licenses
Up until 2022, they primarily used the GPL/LGPL licensing to open-source their product. However, to protect logos and links, they moved to a dual licensing model under the Z Public License (ZPL) and the Affero General Public License (AGPL).
The ZPL is a license Nature Easy Corp (the original name of the company) created. The AGPL is a free software license that serves to protect the rights of the users of the software. It is meant to ensure that the users have the freedom to use, study, share, and modify the software. Their site has quite a writeup on Open-Source licensing and why they chose what they did.
Installation
Let’s start with a docker installation. We can follow the steps from the installation page
I’ll pull a recent release
builder@builder-T100:~$ docker pull easysoft/zentao:18.10
18.10: Pulling from easysoft/zentao
b7f91549542c: Pull complete
6fc18e9d8534: Pull complete
2983c1dd6d30: Pull complete
dd480174791a: Pull complete
a8847cac3d8f: Pull complete
5a3dfd58e1ad: Pull complete
2660e43392bd: Pull complete
d15bee56465f: Pull complete
a43092a20eb7: Pull complete
4e8dfdabdb52: Pull complete
4f4fb700ef54: Pull complete
0a7ac94fb350: Pull complete
8ee05c68845c: Pull complete
ce2b53f47bc2: Pull complete
4443e5c4fa61: Pull complete
e3ea9361327c: Pull complete
660fcd97fabe: Pull complete
Digest: sha256:65cd5f01db6d18e89c6594df30f7613a10cfea9016e2bc2adb05506f686991a2
Status: Downloaded newer image for easysoft/zentao:18.10
docker.io/easysoft/zentao:18.10
I’ll create a couple of docker volumes
builder@builder-T100:~$ docker volume create zentaodb
zentaodb
builder@builder-T100:~$ docker volume create zentaodata
zentaodata
Now I can use those values to launch an instance
builder@builder-T100:~$ sudo docker run --name zentao -p 4490:80 -v zentaodata:/www/zentaopms -v zentaodb:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=MyDbP4ssW0rd -d easysoft/zentao:18.10
4e6ca2cd2f8bf74e8e3faa7956a8715219bbc6e9489ecda11d55a04e862ae1bd
I’ll now create an A record
$ cat r53-zentao.json
{
"Comment": "CREATE zentao fb.s A record ",
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "zentao.freshbrewed.science",
"Type": "A",
"TTL": 300,
"ResourceRecords": [
{
"Value": "75.73.224.240"
}
]
}
}
]
}
$ aws route53 change-resource-record-sets --hosted-zone-id Z39E8QFU0F9PZP --change-batch file://r53-zentao.json
{
"ChangeInfo": {
"Id": "/change/C06168121RGUQIZZSA4B9",
"Status": "PENDING",
"SubmittedAt": "2024-02-13T01:55:19.036Z",
"Comment": "CREATE zentao fb.s A record "
}
}
Once applied, I can create an external endpoint and rule to send Ingress traffic there
$ cat ingress.zentao.yaml
---
apiVersion: v1
kind: Endpoints
metadata:
name: zentao-external-ip
subsets:
- addresses:
- ip: 192.168.1.100
ports:
- name: zentao
port: 4490
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: zentao-external-ip
spec:
clusterIP: None
clusterIPs:
- None
internalTrafficPolicy: Cluster
ipFamilies:
- IPv4
- IPv6
ipFamilyPolicy: RequireDualStack
ports:
- name: zentao
port: 80
protocol: TCP
targetPort: 4490
sessionAffinity: None
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
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: zentao-external-ip
generation: 1
labels:
app.kubernetes.io/instance: zentaoingress
name: zentaoingress
spec:
rules:
- host: zentao.freshbrewed.science
http:
paths:
- backend:
service:
name: zentao-external-ip
port:
number: 80
path: /
pathType: ImplementationSpecific
tls:
- hosts:
- zentao.freshbrewed.science
secretName: zentao-tls
$ kubectl apply -f ./ingress.zentao.yaml
endpoints/zentao-external-ip created
service/zentao-external-ip created
ingress.networking.k8s.io/zentaoingress created
When I was unable to connect, I realized that ZenTao crashed on startup
builder@builder-T100:~$ docker logs zentao
09:50:05.75
09:50:05.75 Welcome to the Easysoft ZenTao 18.10 container
09:50:05.75 Subscribe to project updates by watching https://www.zentao.net
09:50:05.75 Submit issues and feature requests at https://www.zentao.net/ask.html
09:50:05.75
09:50:05.76 INFO ==> Prepare persistence directories.
09:50:05.95 INFO ==> Render php.ini with environment variables.
09:50:05.96 INFO ==> Check zentao data owner...
09:50:05.97 INFO ==> Render apache sites config with envionment variables.
09:50:05.98 INFO ==> Prepare custom extensions.
09:50:06.01 INFO ==> Check whether the MySQL is available.
09:50:06.01 INFO ==> Check whether the Apache is available.
09:50:07.02 WARN ==> Apache: Waiting MySQL 1 seconds
09:50:07.02 WARN ==> Sentry: Waiting Apache 1 seconds
09:50:09.02 WARN ==> Apache: Waiting MySQL 2 seconds
09:50:09.02 WARN ==> Sentry: Waiting Apache 2 seconds
09:50:13.03 WARN ==> Apache: Waiting MySQL 4 seconds
09:50:13.03 WARN ==> Sentry: Waiting Apache 4 seconds
09:50:21.03 WARN ==> Apache: Waiting MySQL 8 seconds
09:50:21.03 WARN ==> Sentry: Waiting Apache 8 seconds
09:50:37.04 WARN ==> Apache: Waiting MySQL 16 seconds
09:50:37.04 WARN ==> Sentry: Waiting Apache 16 seconds
09:51:09.04 WARN ==> Apache: Waiting MySQL 32 seconds
09:51:09.05 WARN ==> Sentry: Waiting Apache 32 seconds
09:51:09.05 ERROR ==> Apache Maximum number of retries reached!
09:51:09.05 ERROR ==> Apache Unable to connect to MySQL: 127.0.0.1:3306
09:51:09.05 ERROR ==> Sentry Maximum number of retries reached!
09:51:09.05 ERROR ==> Sentry Unable to connect to Apache: 127.0.0.1:80
I changed instead to using Docker-compose:
version: "2.2"
services:
zentao:
image: idoop/zentao:latest
container_name: zentao
# if web response code: 310 ERR_TOO_MANY_REDIRECTS, please use host mode.
# network_mode: "host"
ports:
- "80:80"
- "3306:3306"
# mysql root account default password is '123456'.
# the zentao adminstrator account is 'admin',and init password is '123456'.
environment:
# specifies Adminer account and password for web login database.
USER: "root"
PASSWD: "123456"
BIND_ADDRESS: "false"
extra_hosts:
- "smtp.exmail.qq.com:163.177.90.125"
volumes:
- ./data:/opt/zbox/
restart: always
That worked
And once I switched to English, I could see the login page
The initial password for the “admin” user is “123456”. We can login with that and it prompts us to immediately change passwords
While many of the menus are in English, the setup users and groups were not. Here we see the “Test” user in “Testers” group (ID 3), albeit in Chinese
While I couldn’t read the Plugins, the links went to a Plugin marketplace that converted it to English for me
Projects
Let’s start by creating a Project of type Scrum
and set some details
I now have a project, but it’s a bit empty
Let’s create our first Sprint (Iteration)
Now created, we can click “Create task”
I’ll fill in some details
I now have a spike ticket in the sprint
In the details for the task, I can see duration and other details
I’ll click start and enter a time estimate
We can see we have moved into the state of “Doing” with a start time (albeit not my timezone)
Clicking “Create child task” gives me a pane with a lot more options
System settings
I realized the default timezone was set to Shanghai. We can change that in admin/timezone
Users
We’ve been using the admin user and a “test test” user. Let’s create a real identity we can use
I’ll want to configure email settings. I start in Admin Message Mail
I can now add my SMTP details. Here I’ll use Sendgrid
It saved, but I got a warning about my users missing email addresses
I’ll update my admin user email
Now I see an email sending test option on there
I did a test
But got an error
I changed the port and protocol
This time it worked just fine
and i could see a test mail
I started to add the user but realized I would struggle on Privilege Groups
I can now save the user (though it’s a guess on my part for groups)
I thought perhaps it would use email for password resets, however, if a user tries to login and selects “Forgot password” they see this
Departments
I had to use a translator to realize “禅道软件” translated to “ZenTao Software”. So these sub menus are under “ZenTao Software”
I added some sub-departments
If I switch back to Admin, I can assign users to Departments
I can see the Department now in the User’s profile
This applies to the index page
Translated
Later, I realized we can change our company name in settings
Which I could verify in the Departments definition area
Modules
We can create Modules to assign to our Stories
We can more than five after saving. Modules can also have submodules
We can then use them when defining a story
What I tend to call Sprints or Iterations, they call “Project Plans”. So here we can create a 1 month Project Plan
For which we can then link our stories
Screens
There are “BI” Screens that if we let Google Translate kick in, we can view
So while they are in Chinese
Using translate:
Spaces
We can create a “Space”
With the space created, we can create a Kanban board
From there we can create a Card
I’ll create an initial Spike card for auditing systems
Like one would expect, I can drag the card around to change states
If I go to settings, I can change WIP settings
Such as setting a WIP limit on “doing” / active
For instance, if I set to 2 and create 3 cards in “Doing” we can see we exceeded our Work In Progress limit
Backups
We can see daily backups under the Backus tab
I can then copy from the container:
I’ll copy the files down and tgz them
$ docker cp zentao:/opt/zbox/app/zentao/tmp/backup/202402190030227.code /tmp/zentao-202402190030227.code
$ docker cp zentao:/opt/zbox/app/zentao/tmp/backup/202402190030227.file /tmp/zentao-202402190030227.file
$ docker cp zentao:/opt/zbox/app/zentao/tmp/backup/202402190030227.sql.php /tmp/zentao-202402190030227.sql.php
$ tar -czvf /tmp/zentao-202402190030227.tgz /tmp/zentao-202402190030227.code /tmp/zentao-202402190030227.file /tmp/zentao-202402190030227.sql.php
And now I have a nice compressed backup I could send off to a NAS or other backup medium
$ ls -ltrah /tmp | grep zentao
-rwxrwxrwx 1 builder builder 5.7M Feb 18 10:30 zentao-202402190030227.sql.php
drwxrwxrwx 6 builder builder 4.0K Feb 18 10:30 zentao-202402190030227.file
drwxrwxrwx 16 builder builder 4.0K Feb 18 10:30 zentao-202402190030227.code
-rw-rw-r-- 1 builder builder 87M Feb 18 12:58 zentao-202402190030227.tgz
Pricing
There is quite a lot included in the free community edition. However, if you want some features like Task or Project Gantt Charts, you need to move to a Biz or Enterprise edition license
Seems there is a chatbot (mine is called Philip as you see above), that might offer a 10% discount.
Summary
I came into this post thinking I had a weird foreign app that would likely just be a fail. I really was taken back by how full-featured this entire suite is. Once I got past my English-bias - getting scared and confused by non-ASCII defaults - I found it encompasses everything I could want in a Project and Product management suite. Often, we get tools focused on PjM that are very agile-centric (JIRA, Rallydev, Azure Work Items) but then they do little to account for Waterfall, Product release groupings or cost. Here we get it all. For the small cost of having to work through some Chinese Simplified menus (most I could tweak when it was key), we land with something more than useable for tracking and rolling out projects.
While not perfect and at times using terminology outside the norm, it is fully Open-Source so one can take it and tweak it to one’s desire. I’ll likely continue to experiment with this and add it to my core tools. I would love to circle back on helm eventually or at least use an external PostgreSQL, but for now, this version is very usable as hosted in Docker and exposed through Kubernetes.