Self-hosted service
docat-org/docat avatar
docat-org/docat

docat: serving several versions of the same documentation from one container

Host your docs. Simple. Versioned. Fancy.

911 stars55 forksTypeScriptMIT

At a glance

What is it?
docat is a self-hosted service for hosting static documentation with several versions per project. It does the hosting and the version switching, and leaves the writing to tools such as mkdocs, sphinx or mdbook.
Who is it for?
docat fits teams that already generate static documentation and need to serve several versions of it from one self-hosted container, with version tags and a token based claim flow. It does not fit teams that want to author documentation in the browser or that need per-user accounts.
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?
Yes. The repository last received commits 43 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why hosting a single version of the docs is not enough

Static site generators are good at one thing: turning a folder of Markdown or reStructuredText into a directory of HTML. They are less good at the second requirement that teams actually hit. A product ships 1.0, then 1.1, then 2.0, and users on older releases still need the guide that matched the release they installed. The project README puts it plainly, saying that when generating static documentation using mkdocs or sphinx, hosting just one version of the docs might not be enough, and that many users might still use older versions and might need to read those versions of the documentation.

docat is the answer to that narrow problem. It is a self-hosted service that stores several documentation projects, each with several versions, and serves them over one URL with an interface for moving between versions. The stated design decision with docat was to keep the tool as simple as possible, and that shows in the layout: a Python backend under docat/, a TypeScript frontend under web/, and a Dockerfile that assembles both into a single image.

So the person who adopts docat is a developer or platform engineer who already writes documentation with mkdocs, sphinx, mdbook or something similar, and now needs a place to put the output. The person who expects a documentation editor is in the wrong place, and the usage notes say so directly.

One image, two processes, nginx in front of a Python API on port 5000

The Dockerfile shows how the pieces fit together. A first stage builds the frontend from node:24-slim, copying web/package.json and web/yarn.lock, running yarn install --frozen-lockfile, and then yarn build. A second stage prepares the backend on python:3.14-slim, pulling uv and uvx from the ghcr.io/astral-sh/uv:0.10.4 image and running uv sync --no-install-project --no-dev. The final stage installs nginx, dumb-init, libmagic1 and gettext with apt-get, creates /var/docat/doc and /var/www/html, and copies the built frontend into /var/www/html so it is served as plain static files.

The start command reveals the two processes that matter. Nginx serves the frontend and the uploaded documentation, while the Python application runs under uvicorn on port 5000 behind it. The command also runs envsubst on the nginx default config, substituting the MAX_UPLOAD_SIZE value before nginx starts, which is why that variable appears in the Dockerfile defaults as well.

One detail is worth noticing for anyone planning a fork. The repository is described as TypeScript, and the frontend build dominates the top of the Dockerfile, but the backend is Python and the API lives there. The compiled JavaScript bundle needs no interpreter at runtime, while the Python side owns the API and the storage of uploaded archives, so a deployment pulls in two toolchains even though the end user sees one container.

Pushing a first version with docatl and tagging it latest

The quickest path to a running instance is the container. The README gives a run that keeps the docs, the nginx configs and the tokens database in a mounted volume, and it notes that the tag ghcr.io/docat-org/docat:unstable picks up the latest changes.

sh
mkdir -p docat-run/doc
docker run \
  --detach \
  --volume $PWD/docat-run:/var/docat/ \
  --publish 8000:80 \
  ghcr.io/docat-org/docat

Once that is running, the instance answers on port 80 inside the container, so a browser pointed at localhost:8000 shows the docat interface.

The first real use is pushing a built documentation folder. A separate CLI called docatl handles the interaction with the server, and the README also says interacting with docat can be done through curl, so the CLI is a convenience rather than a requirement. To push the contents of a docs folder as a new project and tag it latest:

sh
docatl push --host http://localhost:8000 ./docs PROJECT VERSION --tag latest

The version string you pass becomes the label readers choose in the interface, so the naming scheme deserves a decision before the first push rather than after. The tag argument is what marks a version as current, and the same argument shape lets you push a second version of an existing project once the first is in place.

The claim token is the whole permission model, so nginx has to cover the write endpoints

Out of the box docat trusts everyone. The README states that by default, anyone can upload new documentation or add a new version to documentation. A project can be claimed, and a claim returns a token that must then be used to add or delete versions.

That is a shared secret rather than a user model, so the real security boundary is the reverse proxy in front of the service. The recommended hardening for a public instance is http basic auth on the write methods, which leaves reads open while POST, PUT and DELETE require a password. The example configuration in the README puts the Python backend behind an upstream block and wraps the /api location in a limit_except clause that excludes GET and HEAD:

code
  upstream python_backend {
      server 127.0.0.1:5000;
  }

Both the custom nginx config and the.htpasswd file are mounted into the container at the paths the image expects:

sh
  docker run \
    --detach \
    --volume $PWD/docat-run:/var/docat/ \
    --volume $PWD/nginx/default:/app/docat/docat/nginx/default \
    --volume $PWD/nginx/.htpasswd:/etc/nginx/.htpasswd \
    --publish 8000:80 \
    ghcr.io/docat-org/docat

Two things follow from that arrangement. The password file is read by nginx, not by the Python process, so rotating a password means restarting the container rather than calling an API. And because the auth decision is made per path and per method, a misconfigured location block can leave a write endpoint unprotected even though the rest of the API is guarded.

config.json restyles the pages, MAX_UPLOAD_SIZE caps each archive

Two knobs are available after the fact. The first changes the chrome around the hosted documentation. A config.json file is created and mounted into the container with the volume option --volume $PWD/config.json:/var/docat/doc/config.json, and it accepts header and footer markup:

json
{
  "headerHTML": "<h1 style='color: #115fbf;'>Custom Header HTML!</h1>",
  "footerHTML": "CONTACT: <a href='mailto:[email protected]'>Maintainers</a>"
}

The values are injected as raw HTML, so the file is the place for a banner, a support address or a version notice that should appear on every project page. There is no template file to edit in the repository, which keeps the change out of a fork but also means the styling options are limited to whatever the two fields allow.

The second knob is the environment variable MAX_UPLOAD_SIZE, described as limiting the size of individual archives posted to the API, with a default of 100M taken from the Dockerfile. The entrypoint runs envsubst against the nginx default config, so the same variable feeds the client_max_body_size directive in the proxy block, meaning one setting changes what the API accepts and what the proxy will pass through. Raising it allows bigger archives and, as a side effect, longer uploads and more disk in the mounted volume.

docat hosts the docs, it does not write them, and the filesystem is the database

The clearest limitation is stated at the top of the usage section: docat does not provide any way to write documentation, and its sole responsibility is to host documentation. The README points to mkdocs, sphinx and mdbook as tools for producing those files. If your requirement includes authoring, reviewing and editing inside the browser, docat is the wrong project.

The second constraint is the storage model. Uploaded documentation lives in a directory inside the container, mounted at /var/docat, which the run command in the README describes as holding the docs, the nginx configs and the tokens database. Everything is filesystem based. That makes backup simple and data loss simple too, since a container start without the volume starts from an empty directory. The consequence is that there is no cross-region replication, no queryable database and no tiered storage; the mounted volume is the entire backup story.

The third is the granularity of the credential. A claim returns a single token used to add or delete versions, so everyone holding push rights for a project shares one secret, and the per-user audit trail of who uploaded which version is not something the README describes. For a small internal documentation service that is a reasonable trade. For a setup where several teams publish into the same instance, it is the first thing to revisit.

Release 1.6.1, 1.6.0 and 1.5.4 point at a small, slow-moving codebase

The repository is not archived, and the last push was on 2026-09-10. The three most recent releases are 1.6.1 on 2026-09-10, 1.6.0 on 2026-06-22 and 1.5.4 on 2026-03-11, the last of which the release note labels as favicon support. The version numbers move in small steps rather than large jumps, which fits a tool whose stated goal is to stay simple.

For an operator, an upgrade means pulling a newer image tag and rerunning the container against the same volume, so the thing to check on a version bump is whether the on-disk format of existing projects still loads in the newer build. The docs folder inside the repository holds the getting started guide referenced from the README at doc/getting-started.md, and the container build copies that same file into the frontend assets, so the guide travels with the image.

The licence field on the repository says MIT, which for a self-hosted internal service means you can run and modify it without publishing changes you would rather keep private. It comes with no warranty of any kind, so treat the container as software you are responsible for operating rather than a service with a support desk behind it.

Editorial conclusion

docat fits teams that already generate static documentation and need to serve several versions of it from one self-hosted container, with version tags and a token based claim flow. It does not fit teams that want to author documentation in the browser or that need per-user accounts. Before adopting it, confirm that the version labels you push match the ones your users already refer to, that the volume mounted at /var/docat is in your backup set, and that http basic auth covers /api before the instance is reachable from outside. The last push was on 2026-09-10 and the licence field on the repository says MIT, so the code is short enough to read and to fork.

Frequently asked questions

How do I install docat and run it on my own machine?

Run the published container image ghcr.io/docat-org/docat with a volume mounted at /var/docat and publish port 8000 to port 80, then open localhost:8000 in a browser. The project also documents a local development setup that starts the Python backend with uv and the frontend with yarn.

Does docat let me write documentation, or only host it?

Only host it. The usage notes state that docat does not provide any way to write documentation and that its sole responsibility is to host documentation, pointing to mkdocs, sphinx and mdbook for the authoring step.

How do I control who can upload new versions of a project?

A project can be claimed, and the claim returns a token used to add or delete versions. When hosting publicly, the recommended protection is http basic auth on POST, PUT and DELETE calls, with the .htpasswd file and a custom nginx config mounted into the container.

What is the largest documentation archive I can upload?

The MAX_UPLOAD_SIZE environment variable limits the size of individual archives posted to the API, and the Dockerfile sets a default of 100M. The same value is substituted into the nginx config at container start.

Official sources

  1. docat-org/docat on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/docat-org-docat.svg)](https://hysenlabs.com/projects/docat-org-docat)

Was this useful?