# The image build deletes part of the repository and re-clones it unpinned

> A self-hosted board where lanes are directories and tasks are files, with a build that re-fetches its editor component from a fork at build time, a nightly job that deletes unreferenced images, and a base path setting that silently breaks the installable app.

**BaldissaraMatheus/Tasks.md** — A self-hosted, Markdown file based task management board

- Repository: https://github.com/BaldissaraMatheus/Tasks.md
- Website: https://hub.docker.com/r/baldissaramatheus/tasks.md
- Stars: 2,200 · Forks: 106
- Language: JavaScript
- License: MIT
- Published: 2026-09-30 · Updated: 2026-09-30 · Language: en
- Canonical page: https://hysenlabs.com/projects/baldissaramatheus-tasks-md

## The build deletes a directory and re-clones it at build time

The container build does something unusual in the middle of the frontend install. It removes a whole component directory from the copied source tree, and then clones it again from a fork hosted under the same person's account:

```
RUN rm -r src/components/Stacks-Editor
RUN git clone https://github.com/BaldissaraMatheus/Stacks-Editor src/components/Stacks-Editor
RUN cd src/components/Stacks-Editor && npm ci --no-audit
```

No commit, tag or branch is named. Whatever the fork's default branch points at is what goes into the image, and the dependency tree that resolves from it is installed separately from the application's own.

The reason is not hard to infer. The editor component is a fork of an existing rich text editor, and the project's choice has been to keep its changes out of the main repository and pull them in at build time instead. That keeps the diff small and the main tree readable. The cost is that the image is no longer reproducible from a commit: two builds of the same tag can contain different editor code, and a change in that fork reaches every user's next pull without appearing in this repository's history.

The same repository appears twice more, which explains why a source checkout has to be recursive. There is a submodule file in the root, and the readme's source-install instruction uses the recursive clone flag. So one component is reachable as a submodule, as a build-time clone, and as a directory in the tree, and the build chooses the middle path.

Everything else in the build stage is ordinary. Git is installed because of that clone, the frontend is installed with dev dependencies omitted, and the backend is installed in a second directory with its own step.

## Node 18 builds the image and Alpine's own Node runs it

The two stages of the image use different Node installations, and only one of them is pinned. The build stage names an exact build and an exact Alpine release. The final stage is a bare Alpine image that installs the Node package from the distribution's own repositories.

So the interpreter that installs the dependencies is a specific version, and the interpreter that runs the installed code is whatever Alpine ships for that release, resolved at build time and free to change when the image is rebuilt. Nothing in the file records which one it ended up with.

That matters more than it would for a script. The application's dependencies are installed under the build stage's Node, and the backend serves requests and reads files under the runtime stage's Node. An engine constraint that holds for one and not the other produces an error at the point of running rather than at the point of installing.

The rest of the final stage is short and explains the two volumes. The data directory and the configuration directory are declared as volumes so they can be mounted from the host, the stylesheet directory is created empty so a custom file has somewhere to live, and the work directory is the API directory. Port 8080 is exposed.

Two details in that stage are worth naming. The container runs as root, and it does so deliberately enough to be stated. And the entrypoint is written in shell form, which means the process tree starts with a shell rather than the application, so signals and restarts pass through an interpreter that has to forward them.

## A background job deletes every image no task mentions

One of the four environment variables is a deletion schedule. After a given interval, the application removes all local images that are not present in any task. The interval is measured in minutes, the default is 1440, which the readme notes is exactly twenty four hours, and setting it to zero disables it.

So the default behaviour of a fresh install is to sweep once a day, and the thing it sweeps is unreferenced files in the data directory. For an application whose data model is files on disk, that is a dangerous default for anything a user stages. Download an image, attach it to a draft task you have not saved yet, and a day later the file is gone and the draft points at nothing. Attach nothing at all and the same happens to an image you were about to use.

Nothing in the documentation warns about this, and the variable's own description is written from the application's point of view rather than the user's, which is how a destructive default stays invisible.

The other two variables are about file ownership, and they carry their own warning. If no user and group identifier is supplied, the container creates all the files and directories as root, which is stated plainly and points at the documentation for the usual Linux server convention of one thousand for both. On the single-command install both are set to one thousand; in the compose sample they are also set. Everywhere else, the file ownership of your board depends on you remembering.

The third variable is cosmetic: a title shown below the header and in the browser tab, only at the root path.

## A subpath deployment and an installable app are mutually exclusive

The application can be installed as a progressive web app, and it can run behind a reverse proxy under a subpath. The readme states that these two features do not work together: the base path variable exists to serve the app from a subpath, and the note says the installable app does not work when that variable is set to anything other than the root.

That is a real trade-off rather than a defect, and it is documented in the right place. It is worth internalising as a class of deployment question, because the same pattern shows up in most single page applications: the service worker scope and the URL base have to agree, and a reverse proxy subpath moves the base.

The compose sample is where the defaults are least visible. It opens with a version key that current Compose releases accept and ignore, names an image with no tag at all, and sets only the two ownership variables. The base path, the title and the cleanup interval are all absent, which means the sample runs on the root path, with the daily image sweep enabled, under a floating image tag.

The single-command install sets the cleanup interval explicitly to the default value and leaves the base path and title empty, so the two installation paths in the readme do not agree on what a minimal configuration is. Both are documented as complete, and the compose file says to use the Docker section above as the reference for variables and volumes.

Language handling sits alongside this: the interface locale is detected from the browser and then persisted per user, which is the right behaviour for a shared board.

## A major version upgrade needs its own guide, and the last commit is the release

The readme carries a section for people upgrading across the major version boundary, and it says the upgrade requires some tweeks for it to work properly, then points at a migration guide in the repository root. The spelling is the project's own.

That is a fair thing to require and an expensive thing to skip. When the storage layout is the data model, a major version is a data migration, and the readme's own file structure section explains why: every lane is a directory and every task is a file, so anything that changes about how those are laid out has to move real files.

The release history is short. Version 3.2.1 in late December 2025, 3.2.2 a few days later, and 3.3.0 in March 2026. The 3.3.0 release was published at the same second as the repository's last commit, so the final commit of record is the release itself rather than anything that followed it.

Since then the default branch has not moved. That is consistent with what the contribute section says, which is that this is a low maintenance project whose scope of features and support is purposefully kept narrow so that longer term maintenance is viable, and that pull requests for bugs and quality-of-life improvements are welcome while features that significantly increase the scope are not.

It is a coherent policy and it has a cost: a project whose storage format is its own documentation will accumulate migration work against nobody's deadline.

## Every lane is a directory, so a subdirectory is a different board

The storage model is the feature. Each lane you create in the interface is a directory on disk, and each task is a file in it. The readme shows the same board twice, once as lanes and cards and once as the resulting directories and files, which is the clearest possible statement that the interface is a view.

The consequence people miss is in the next paragraph: sub-directories can be opened as their own projects. Serve the app at a different path and that directory becomes a separate project with its own lanes and tasks. So one directory tree can hold several boards, and moving the application to a subpath is what selects between them rather than changing anything about the data.

The readme also links an issue showing what the same files look like opened in a note-taking application with markdown support. That is the strongest argument for the design: your task list is not trapped in the application, and an editor, a synchroniser or a version control system can all read it.

The interface side is thin on purpose, which follows from the same logic. The application is built with a reactive JavaScript framework for the front end and a small Node server for the back end, plus the forked editor component for text entry, and it serves its stylesheet files as-is rather than bundling them.

That last choice is the theming mechanism. Because the stylesheet is served unmodified from disk, dropping a file into the configuration directory overrides the bundled one, which is why customisation is a fourteen-variable interface rather than a settings page: an accent colour, a foreground, four background layers, and seven alternates.

## A source install means editing package.json and running two processes

The from-source path is four steps and has two differences from the container that are easy to miss. The clone has to be recursive, because of the submodules discussed earlier. And you run the front end and the back end as two separate processes, from their own directories, in two terminals, each with an install step and a start step.

The configuration difference is the bigger one. The readme says the environment variables are set inside the package manifest for both directories, and they are the same ones the Docker section lists plus two more: one for the configuration directory and one for the tasks directory. So to run from source you edit a manifest to point the application at your files, rather than passing anything on a command line or in a compose file. The two extra variables have no equivalent in the container, where the volumes fix those paths instead.

That arrangement is what a project with no build step for its own configuration tends to look like, and it is fine for a development checkout. It does mean a source install and a container install are configured in two different places, and a reader moving between them has to know that.

The root also carries a keyboard shortcuts document, an entrypoint script, the migration guide and a node version file. The version file is the one that matters for a source install, since the build stage of the image pins a Node version that nothing in the front end or back end is otherwise obliged to match.

## Conclusion

Tasks.md is a good fit if you want a kanban board you can edit in a text editor and keep in git, and the file layout means the board outlives any particular interface. Read two things before the first run. The image cleanup job deletes every local image no task references, on a timer, with the timer on by default, so decide whether your images are all attached before you drop any in. And the base path variable disables the installable app, which is a real trade rather than a bug. For anyone building from source rather than pulling the image, note that the build re-clones the text editor component from a fork at whatever its default branch is, so two builds a week apart are not guaranteed to be the same. The maintainer states plainly that this is a low maintenance project with a deliberately narrow scope, and the last commit was on 2026-03-08.

## FAQ

### How does Tasks.md store my tasks?

Every lane you create is a directory on disk and every task is a file inside it, written as Markdown. A sub-directory can be opened as its own separate project, and the readme links an issue showing the same files opened in a note-taking application.

### What does LOCAL_IMAGES_CLEANUP_INTERVAL do in Tasks.md?

It is a deletion schedule. After the given interval in minutes, the app removes all local images that are not present in any task. The default is 1440, which the readme describes as exactly twenty four hours, and setting it to zero disables the cleanup.

### Why does PUID and PGID matter for the Tasks.md container?

They set the user and group identifiers that own the files and directories. The readme warns that if you do not assign them, Docker creates all the files and directories as root, and it points at the usual convention of 1000 for both.

### Does Tasks.md work behind a reverse proxy on a subpath?

Yes, with a caveat. The BASE_PATH variable serves the app from a subpath, and the readme notes that the installable progressive web app does not work when the base path is anything other than the root.

### How do I install Tasks.md from source?

Clone the repository recursively, open a terminal in the frontend directory and another in the backend directory, and run npm install and npm start in both. The environment variables are edited into the package manifest for each directory, including two directory paths that have no equivalent in the container.

## Sources

- [BaldissaraMatheus/Tasks.md on GitHub](https://github.com/BaldissaraMatheus/Tasks.md)
- [License: MIT](https://github.com/BaldissaraMatheus/Tasks.md/blob/main/LICENSE)
- [Project website](https://hub.docker.com/r/baldissaramatheus/tasks.md)
- [README](https://github.com/BaldissaraMatheus/Tasks.md/blob/main/README.md)
- [Releases](https://github.com/BaldissaraMatheus/Tasks.md/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/baldissaramatheus-tasks-md
