# Medama ships three licences in three directories, and its Dockerfile trusts every task file

> A cookie-free analytics server in Go with a sub-1KB tracker and a 256 MB memory floor, packaged both as a single binary and as a distroless image. The build is pinned by digest and then installs a tool manager from a URL.

**medama-io/medama** — Self-hostable, privacy-focused website analytics.

- Repository: https://github.com/medama-io/medama
- Website: https://oss.medama.io
- Stars: 643 · Forks: 21
- Language: Go
- License: not declared
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/medama-io-medama

## core and dashboard are Apache 2.0, the tracker is MIT, and the root has none

The licence section splits the repository in three rather than stating one grant.

The `core` and `dashboard` directories are under the Apache License 2.0, each with its own licence file. The `tracker` directory is under the MIT License, with its own licence file too. No licence file appears at the top level, and the repository itself records no licence.

The split is defensible and reads as deliberate. The tracker is the script you copy into other people's pages, so it needs the permissive, attribution-light grant. The server is the part you run, so it gets the licence with an explicit patent grant.

What a reader cannot do from the root is answer one question without opening three files: what licence covers the thing I am about to use. For a project whose main argument is privacy and minimalism, having the licence be the one part you have to assemble yourself is a small inconsistency.

What the README does give you plainly is the privacy claim. No cookies, no IP addresses, no additional identifiers, a tracker under one kilobyte, and compliance named as GDPR and PECR among others. Those are stated as facts about what the software does, not as a policy promise.

## Three releases, and the newest one is seven months older than the last commit

Three release tags are visible: v0.6.0 on 2025-08-11, v0.6.1 on 2025-08-17 and v0.6.2 on 2026-01-18.

The first two are six days apart. The third is five months after the second. The last push to the default branch is 2026-08-07, which is seven months after the newest tag.

So the shape of the history is two quick patch releases, a long gap, then a gap that continues. Whatever has landed on the default branch since January is unreleased by the tag list, and there is no nightly or rolling tag mentioned anywhere to distinguish it.

That matters more for this project than for a library, because the pitch is a self-hosted binary. Somebody running the newest release is seven months behind the code, and somebody reading the repository to understand the software is reading something newer than what they would download.

Around 640 stars, 21 forks and 34 open issues is a modest fork ratio for a self-hostable service, which fits a project people deploy rather than vendor.

The distribution story is a single binary with no external dependencies, plus a website, an installation guide, a live demo instance and a Discord.

## The build curls a tool manager from a URL and then trusts every task file

The container build is careful in the first half and permissive in the second.

The Dockerfile front end is pinned to a digest. The build base is pinned to a digest as well, a manylinux image. Then the tool manager arrives like this:

`RUN curl https://mise.run | sh`

No digest, no checksum, no review step. The script is fetched over HTTPS from a stable URL and executed, with an experimental flag switched on in the environment.

Immediately after, four mise configuration files are copied, one from the root and one from each of the three component directories, and the build runs:

`RUN mise trust -a -y && mise install go bun`

The `-a` trusts every configuration file it finds and the `-y` answers yes, so no task defined in any of those four files is ever shown to a human before it runs. That is what makes the layered caching work, since the runtimes can be installed before the source is copied. It is also the point where a build system stops being reviewable and starts being trusted.

The release itself is produced by a task, `mise run //core:release:docker`. The task body lives in the core component's mise configuration, which is not part of what is documented, so the actual compile step for the binary you ship is not visible in the README.

## The step labelled verify environment variables only echoes them

Two build arguments carry version information. One is the version, defaulting to the literal string `development`. The other is a commit SHA, defaulting to the same literal string.

Both are promoted to environment variables early in the build, and later there is a step whose comment reads verify environment variables. That step is two echoes:

`RUN echo "VERSION=${VERSION}" && echo "COMMIT_SHA=${COMMIT_SHA}"`

It prints the values and exits. Nothing is checked, nothing is compared against a tag, and a build where both arguments were left at their defaults succeeds exactly as a build with real values does. The only effect is that the strings appear in a build log.

This is a common shape in container builds and harmless in isolation. It is worth noticing here because these two variables are the only place a binary learns where it came from, and a reader of the build file would reasonably take that comment at face value.

The layer ordering is otherwise well done. Go module files, the root package manifest, the lockfile and each component manifest are copied before the source, dependencies are downloaded against the cache, and only then does the full tree get copied and compiled.

## A Python manylinux image builds the Go binary, and the runtime is distroless

The build base is a manylinux image at version 2.34 for x86-64, pinned by digest.

manylinux images exist to produce portable Python wheels, and they are named after an older glibc baseline than most current distributions. Using one to build a Go binary and a Bun frontend is unusual, and the reason is visible in the next line: the build installs packages with `yum`, which is a Red Hat family package manager rather than the apt that a Debian-based Go image would use.

So the base is chosen for its glibc floor, not for its Python. The effect is a build stage with a newer libc than the 2.34 it is named for, which is at least consistent.

The final image is a different shape entirely: a distroless Debian 12 image with the C runtime included, pinned by digest, carrying Open Container labels that name the source repository and describe the software as cookie-free and privacy-focused.

That gives the project two delivery stories at once. The README sells a single binary with no external dependencies that runs on a virtual machine with 256 MB of memory. The container is a distroless image with no shell and no package manager.

Both are true and they serve different people. Worth knowing which one you are installing before you decide how to configure it.

## The root manifest depends on a linter and trusts a bundler it never declares

The root `package.json` is small and slightly odd.

It is named `medama-analytics` and marked private. It has exactly one dependency, `@biomejs/biome`, pinned to an exact version rather than a range. A formatter and linter sits in `dependencies` rather than `devDependencies`, in a private manifest that is never published.

It declares two workspaces, `dashboard` and `tracker`. The third component, `core`, is not a workspace, which is consistent with it being the Go service rather than a JavaScript package.

The interesting field is `trustedDependencies`, which lists the linter and `esbuild`. That field is what tells a package manager it may run install scripts for those packages. `esbuild` does not appear anywhere in this manifest's dependency list, so it is trusted here on the assumption that a component manifest declares it.

Trusting a binary bundler to run install scripts is normal, since esbuild needs a postinstall to place its platform binary. It is notable that the trust is declared at the root rather than next to the declaration, because a reader auditing the root sees a package they cannot find.

Lockfile discipline is strict elsewhere: the container build installs with a frozen lockfile, and the linter version is pinned exactly, so a build will not drift.

## An eighteen-entry root with a Fly config and an agent directory

For a product whose pitch is one file and no dependencies, the repository root is broad.

The eighteen entries break down into three component directories, `core/`, `dashboard/` and `tracker/`; three manifests, `package.json`, `bun.lock` and `mise.toml`, plus a `mise.toml` inside each component; the `Dockerfile` and a `.dockerignore`; and configuration for tooling and community.

Three of those entries say something about how the project is run rather than what it is. `fly.toml` is a deployment configuration for one specific hosting platform, committed next to a self-hosting pitch that does not mention it. `renovate.json` configures automated dependency updates. And there is a `.codex/` directory, an agent configuration, sitting alongside `AGENTS.md` and `CONTRIBUTING.md`.

Two files use the same tool. `biome.json` at the root configures the linter, and `.editorconfig` configures whitespace for editors that do not read biome.

The `.github/` directory holds images as well as workflows, since the banner graphics referenced at the top of the README live under `.github/images/` as light and dark variants.

None of this is wrong for a project of this size. It is a reminder that the single binary is the shipping artefact, not the repository.

## Conclusion

Medama is a sensible pick if you want page analytics without cookies, no IP retention and a tracker small enough to forget, and the sub-kilobyte claim plus the 256 MB figure make it easy to trial on a small VM. Two things to sort out before you deploy it. The licences are per directory, so read the one that covers the code you intend to reuse rather than assuming the repository has a single grant. And the build trusts every mise task file without asking, which is fine for your own clone and worth a second look before you build unreviewed branches.

## FAQ

### What license applies to medama-analytics?

It is split by directory. The core and dashboard directories are Apache 2.0 and the tracker directory is MIT, each with its own LICENSE file. There is no licence file at the repository root, so you need to open the one covering the part you plan to use.

### Does medama-analytics use cookies?

No. The stated position is no cookies, no IP addresses and no additional identifiers, with a tracker under one kilobyte, and GDPR and PECR named among the regulations it aims to satisfy.

### How much memory does medama-analytics need?

The README states that it can run on virtual machines with 256MB of memory for most small websites, and describes the setup as a single binary with no external dependencies.

### How is the medama-analytics release built?

The container build pins the Dockerfile front end and the build base by digest, installs a tool manager from a URL, trusts every mise task file without prompting, and then compiles through a task named core:release:docker. Version and commit arguments default to the literal string development.

### What is the newest release of medama-analytics?

v0.6.2, published 2026-01-18, following v0.6.1 in August 2025 and v0.6.0 six days before that. The default branch was last pushed on 2026-08-07, so the branch is ahead of every published tag.

## Sources

- [Issues](https://github.com/medama-io/medama/issues)
- [medama-io/medama on GitHub](https://github.com/medama-io/medama)
- [Project website](https://oss.medama.io)
- [README](https://github.com/medama-io/medama/blob/main/README.md)
- [Releases](https://github.com/medama-io/medama/releases)

---

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