ZenTao: OS PjM

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.


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.


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.


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

I’ll create a couple of docker volumes

builder@builder-T100:~$ docker volume create zentaodb
builder@builder-T100:~$ docker volume create 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

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": ""

$ 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
  name: zentao-external-ip
- addresses:
  - ip:
  - name: zentao
    port: 4490
    protocol: TCP
apiVersion: v1
kind: Service
  name: zentao-external-ip
  clusterIP: None
  - None
  internalTrafficPolicy: Cluster
  - IPv4
  - IPv6
  ipFamilyPolicy: RequireDualStack
  - name: zentao
    port: 80
    protocol: TCP
    targetPort: 4490
  sessionAffinity: None
  type: ClusterIP
apiVersion: networking.k8s.io/v1
kind: Ingress
    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
    app.kubernetes.io/instance: zentaoingress
  name: zentaoingress
  - host: zentao.freshbrewed.science
      - backend:
            name: zentao-external-ip
              number: 80
        path: /
        pathType: ImplementationSpecific
  - 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 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.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:
 09:51:09.05 ERROR ==> Sentry Maximum number of retries reached!
 09:51:09.05 ERROR ==> Sentry Unable to connect to Apache:

I changed instead to using Docker-compose:

version: "2.2"
    image: idoop/zentao:latest
    container_name: zentao
    # if web response code: 310 ERR_TOO_MANY_REDIRECTS, please use host mode.
#    network_mode: "host"
      - "80:80"
      - "3306:3306"
    # mysql root account default password is '123456'.
    # the zentao adminstrator account is 'admin',and init password is '123456'.
    # specifies Adminer account and password for web login database.
      USER: "root"
      PASSWD: "123456"
      BIND_ADDRESS: "false"
      - "smtp.exmail.qq.com:"
      - ./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



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



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



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




Later, I realized we can change our company name in settings


Which I could verify in the Departments definition area



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



There are “BI” Screens that if we let Google Translate kick in, we can view


So while they are in Chinese


Using translate:



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



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


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.


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.

ZenTao OpenSource PjM

Have something to add? Feedback? Try our new forums

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