Tasks.md: a kanban board stored as Markdown files
A self-hosted, Markdown file based task management board
At a glance
- What is it?
- Tasks.md keeps no database. Every lane is a directory, every card is a Markdown file, and the two volumes you mount hold your data and your configuration. That single decision gives you git history, an editor that already exists and a backup that is a file copy, and it costs you a schema, and for some teams that trade is the whole point.
- Who is it for?
- Adopt Tasks.md if you want a board your team can keep running for years with data you can read, diff and back up without a database dump, and read migration-guide.md before you move from a 2.x image. Do not adopt it expecting new features, because the contribution policy explicitly asks for bug fixes and quality-of-life changes only, and the last push was on 2026-03-08.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Activity is slowing. The repository last received commits 6 months ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 2, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Every lane is a directory, every card is a file
The storage model is the product, and the README states it in one sentence: every lane you add in the app is a directory on your filesystem, and every task is a file.
So a board with three lanes named Backlog, Sprint and Done containing one card called "Something something" is three directories and one Markdown file. Nothing else. There is no index, no schema version inside your data, and no database to dump.
What that buys is concrete. The files diff in git, so a task edit is a commit with an author and a message. They open in Obsidian or any editor, and the project links an issue showing what the same board looks like there, which is a nice way to make the point that the data outlives the application. And a backup is a file copy, which is the kind of thing you can automate with the tools you already trust for backups.
There is one feature in that model that deserves attention because it is not obvious. Sub-directories can be opened as their own projects: point the app at a `/backlog` path and it treats that directory as a different project with its own lanes and tasks. That is a hierarchy of boards out of plain folders, with no extra configuration and no namespace to manage.
The honest alternative to all of this is to have no board at all and keep tasks as Markdown files in a repository you edit directly. You lose the kanban view, due dates, tags and the PWA, and you gain zero server to operate and diffable history for free. Tasks.md is the middle option: same files, a view over them.
The subpath proxy and the PWA cannot both be satisfied
The feature list is short, and two entries on it interact in a way the README is unusually honest about.
The app supports being served under a subpath on a reverse proxy, configured through a `BASE_PATH` environment variable. And it can be installed as a PWA. Those two are mutually exclusive in practice, because a progressive web app manifest is resolved from the origin, and if the app lives under a subpath rather than the root, the manifest and the service worker scope stop lining up. The README says it plainly: PWA does not work when `BASE_PATH` is set with anything other than `/`.
That is worth noticing as a class of thing. A self-hosted app that people install to their home screen and a self-hosted app that people reach through a company reverse proxy at `/tasks` are different products, and this one can be either. It is the kind of documentation you only find in a project that has already had somebody file the issue.
The other entry worth a sentence is multilingual support, where the locale is auto-detected from the browser and persisted per user. Per user is the interesting word: the preference is stored per account rather than globally, so a shared instance can show different languages to different people. For a self-hosted team tool that is almost always what you want, and it means the setting survives a locale change on the machine.
The remaining features are conventional and worth listing only so they are accounted for: cards, lanes and tags in a responsive interface, Markdown files as the card format, light and dark themes following the operating system setting, and a single Docker image as the install path.
Two volumes, four variables, and who owns the files
Installation is one container and two mounted directories. The documented command passes more variables than most people need, so the simplest version is a good starting point:
docker run -d \
--name tasks.md \
-e PUID=1000 \
-e PGID=1000 \
-e TITLE="" \
-e BASE_PATH="" \
-e LOCAL_IMAGES_CLEANUP_INTERVAL=1440 \
-p 8080:8080 \
-v /path/to/tasks/:/tasks/ \
-v /path/to/config/:/config/ \
--restart unless-stopped \
baldissaramatheus/tasks.mdThe two volumes are the whole persistence story: `/tasks/` holds the lanes and cards, `/config/` holds configuration including your stylesheet. Everything else in that command is optional.
Set `PUID` and `PGID` on the first run, though. They are the user and group IDs that own the files, the README suggests getting them by running `id` in a terminal, and notes that on Linux distributions it is usually 1000 for both. If you leave them unset, Docker creates all the files and directories as root, which is a five-minute problem now and an annoying one later when you try to read your own board from a shell.
`TITLE` is a display name shown below the header and in the browser tab when you hit the root path, which is how you tell two instances apart. `LOCAL_IMAGES_CLEANUP_INTERVAL` is the least obvious one: after the given number of minutes, the app deletes local images that are no longer referenced by any task, which keeps pasted screenshots from accumulating forever. The default is 1440, exactly twenty-four hours, and setting it to 0 disables the cleanup entirely.
There is a Compose file too, and it is worth reading because it is smaller than the long command and shows which variables are actually load-bearing:
version: "3"
services:
tasks.md:
image: baldissaramatheus/tasks.md
container_name: tasks.md
environment:
- PUID=1000
- PGID=1000
volumes:
- /path/to/tasks:/tasks
- /path/to/config:/config
restart: unless-stopped
ports:
- 8080:8080To run from source instead, clone the repository recursively, open a terminal in the `frontend` directory and another in `backend`, and run `npm install` then `npm start` in both. The same variables are set in each package.json, plus `CONFIG_DIR` and `TASKS_DIR` for the two directory paths.
Node 18 to build, whatever Alpine ships to run
The Dockerfile is short enough to read in full, and it contains three things worth knowing before you build your own image.
First, the build stage pins Node precisely, `node:18.20.4-alpine3.20`, because that is where the frontend is compiled. The final stage is a different thing entirely: `alpine:3.20`, into which it installs `nodejs` and `npm` from Alpine's own package repository with no version pin. So the frontend is built by Node 18 and served by whatever Node Alpine 3.20 currently ships, which is a later major version. That works because the build output is static files, but it means the runtime Node version moves when Alpine moves rather than when you move.
Second, the build clones a dependency from the network. The Stacks-Editor component is removed from the copied source and re-fetched inside the build with a `git clone` of the author's own fork, then installed with npm. Nothing in that line pins a tag or a commit, so the content of a given image build depends on what the branch pointed at that day. Combined with the `.gitmodules` file at the repository root and the `--recursive` flag in the source clone instructions, submodules are part of the build contract in two different places.
Third, the runtime image sets `USER root` explicitly and never drops privileges. Ownership is handled the way the LinuxServer convention handles it, through the `PUID` and `PGID` variables that the entrypoint applies to the mounted volumes, rather than by running the process as an unprivileged user. That is a legitimate pattern for an image whose purpose is to read and write files you own, and it is worth knowing before you decide this container belongs in a hardened environment.
Fourteen colour variables and one mounted stylesheet
Customisation here is a text file, not a build step, and the reason is visible in the stack: `serve-static` is used to serve the CSS files as-is.
That single choice means the theme layer is a file you drop into the config directory and edit. On Docker that is `/config/custom.css`; the stylesheet is then served without being compiled, so a change is a file save and a browser refresh, with no image rebuild and no restart.
Three themes ship by default: Adwaita, which is the default, plus Nord and Catppuccin, and the README suggests switching by replacing the default rather than writing CSS from scratch.
The colour variables are where the design system shows itself, and they are documented one by one. There is `color-accent` for highlights and `color-foreground` for anything needing contrast against the background. Then four background layers, and the numbering is an elevation scale: `color-background-1` is the main page, `-2` is one layer up and is used for the editor code block, dialogs, popovers, lanes and the header, `-3` is two layers up and is used for cards, `-4` is three layers up and is used for buttons and inputs. Finally seven accent slots, `color-alt-1` through `color-alt-7`, which serve as tag colours, with `-1` doubling as the input-error and past-due-date colour and `-3` also carrying the current-date due-date state.
That is a complete and unusually tidy theme surface: fourteen variables covering accents, four elevations, tag colours and the three date states. And the README gives you the next step for anything beyond colour replacement, pointing at `frontend/src/stylesheets/index.css` as the reference for structural changes.
SolidJS in front, Koa behind, and StackExchange's editor
The technology choices are stated with a rationale, which is a good sign, and the pair is worth understanding because it explains the feel of the application.
The front end is SolidJS and the back end is Koa, and the stated goal is a mix of performance and maintainability. That combination is coherent. SolidJS compiles to direct DOM updates without a virtual diff, which suits a board where the main interaction is dragging a card and watching four things update. Koa is a thin HTTP layer over Node's own server, which suits an application whose entire job is reading and writing files and serving static assets. Neither choice needs more machinery than the task has.
The text editor is Stacks-Editor, from StackExchange, which is the editor Stack Overflow and its sister sites use. That detail is more significant than it looks for a Markdown board: the core interaction is writing Markdown with a preview, and the quality of that editor is the difference between pleasant and irritating. It is also why the Dockerfile has to fetch a fork of it during the build, so the component can live inside the project layout.
What is notably absent is as informative. There is no database driver in the stack description, no authentication provider, no background job system and no websocket layer in the README's account of the technology. For a single-tenant self-hosted board where the data is files on a disk you mounted, that is the correct set of omissions.
The repository also ships a keyboard shortcuts document alongside the README, which is a small signal about who the tool is for: people who live in it.
The contribution policy is the maintenance policy
This project tells you its own maintenance policy more directly than most, and the statement is worth quoting rather than paraphrasing.
It describes itself as a low maintenance project, and says the scope of features and support are purposefully kept narrow to ensure longer-term maintenance is viable. Issues and pull requests for bugs and quality-of-life improvements are welcome; features that significantly increase the scope are not. That is an unusual and useful thing to find in a README, because it converts the upgrade question from a guess into a decision you can make from the project's own words.
The dates are consistent with that posture rather than in tension with it. The last push was on 2026-03-08, which is also the date of release 3.3.0. Before that came 3.2.2 on 2025-12-29 and 3.2.1 on 2025-12-26, so three releases inside eleven weeks and then nothing for roughly six months. There is no evidence in that of abandonment; there is evidence of a project where releases happen when there is something worth releasing.
The other thing that tells you how releases go is the upgrade path. Moving from a 2.x container to a 3.x one is not a pull, because the README says it requires some tweaks to work properly and points at a migration guide in the repository. A project with a filesystem data model can still have an application-level breaking change, and having written the guide is what makes that acceptable.
For an evaluator, the conclusion is simple. If you need a task board that will still be there in three years, whose data you can read without the application, and whose maintainers have told you in advance that the feature set will not grow, this fits. If you need features, it is the wrong project, and no amount of container configuration will change that.
Editorial conclusion
Adopt Tasks.md if you want a board your team can keep running for years with data you can read, diff and back up without a database dump, and read migration-guide.md before you move from a 2.x image. Do not adopt it expecting new features, because the contribution policy explicitly asks for bug fixes and quality-of-life changes only, and the last push was on 2026-03-08. Set `PUID` and `PGID` on first run, or the container will create your files as root, and set `BASE_PATH` only if you accept that the PWA stops working.
Frequently asked questions
How does Tasks.md store my tasks and lanes?
Every lane is a directory on the filesystem and every task is a Markdown file inside it, so a board is just folders and text files. The two mounted volumes hold the data at /tasks and the configuration at /config, and sub-directories can be opened as their own separate projects by path.
Can I run Tasks.md behind a reverse proxy on a subpath?
Yes, by setting the BASE_PATH environment variable to the subpath you are serving under. Be aware that the README states the PWA does not work when BASE_PATH is set to anything other than the root path.
Why are my Tasks.md files owned by root?
PUID and PGID are optional, and if you do not set them, Docker creates all the files and directories as root. Set them to your own user and group IDs, which you can read by running `id` in a terminal and which are usually 1000 for both on Linux distributions.
How do I upgrade Tasks.md from version 2 to version 3?
Not by pulling a new image. The README says upgrading from 2 to 3 requires some tweaks to work properly and directs you to the migration guide in the repository. Releases are tagged separately, with 3.3.0 dated 2026-03-08 after 3.2.2 in December 2025.
How do I customise the appearance of Tasks.md?
Place a custom.css in the config directory, which on Docker is the mounted /config volume, and it is served as-is without a rebuild. Three themes ship by default, Adwaita, Nord and Catppuccin, and the documented variables run from color-accent and color-foreground through four background layers to seven color-alt tag colours.
Does Tasks.md allow new features?
The project states it is a low maintenance project with the scope of features and support purposefully kept narrow for longer-term maintainability. Bug fixes and quality-of-life pull requests are welcome; features that significantly increase the scope are not.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/baldissaramatheus-tasks-md)