Skip to content

One command, one machine

The self-hosted build is the whole build: no feature gate, no community edition with the good parts removed, and no outbound call at runtime for anything, including geolocation.

4 GB of RAM, two vCPUs5 GB of disk to startFive containersAGPL-3.0, free, no tier

Three commands, in three places

On the server
git clone https://github.com/micaforge/micaforge
cd micaforge
./install.sh --domain analytics.example.com --email you@example.com

It checks the machine, generates three secrets with openssl, writes .env at mode 600, pulls the store images, builds the server, waits until it reports healthy, and prints your dashboard address. Run it with no arguments to be asked instead, or with --no-tls on a laptop. Running it twice is safe: an existing .env is never overwritten, because the key inside it is the key every stored secret is sealed with.

In your pages
<script defer
  data-site="1"
  data-host="https://analytics.example.com"
  src="https://analytics.example.com/mf.js"></script>

Under 3 KB gzipped, no dependencies, never throws into your page. The setup screen shows the real site id.

Beside your web server
micaforge-shipper --host https://analytics.example.com \
  --site 1 --key-file ingest.key /var/log/nginx/access.log

The third one is what makes the machines visible. Crawlers never run JavaScript, so the snippet above will never see one; their fetches only exist in the access log of the server that serves your site. Run the shipper beside it, or the Citation Gap has only one of its two numbers. The dashboard's setup screen prints this command with your site id and key already filled in.

What is actually running

The five containers and what talks to whatBrowsers and your web server's access log both reach Caddy, which passes everything to the single Micaforge process. That process reads and writes Postgres for identity and configuration, ClickHouse for the event store, and Valkey for sessions, rate limits and the live counter.Browsermf.js, under 3 KBAccess logshipped, every crawlCaddyTLS, static filesmicaforgeone Rust binaryAPI, tracker, workersPostgres 17identity, sites, jobsClickHouseevery event, foreverValkey 8salts, limits, live countpeople, from the trackermachines, from the log
Five containers, one docker compose file, no message broker and no second service to operate. The API, the tracker file and every background worker live in the same Rust process, so there is one thing to restart and one log to read.

What you need

  • A machine with Docker, the Compose v2 plugin and openssl.
  • 4 GB of RAM and two vCPUs. That is the size the shipped configuration is tuned for. The installer warns below 1.8 GB, and it is right to: ClickHouse alone is capped at 1.4 GB, so under about 2 GB the kernel starts killing things.
  • At least 5 GB of disk. The images are around 1.5 GB before a single event is stored.
  • For HTTPS: a domain whose DNS already points at the machine, and port 80 reachable from the internet, because that is how the certificate is ordered.

Nothing else. No account, no licence key, no phone-home, and no outbound request at runtime, which is also why geography needs a MaxMind-compatible database on disk rather than an API call.

Where the memory goes

ClickHouse is the component that will notice a small machine first, and the shipped configuration reflects that. Postgres holds identity, sites, goals, dashboards and the job queue, all of which are small, and it is kept off the ingest path entirely: sites are resolved from a one-minute in-process cache, policy files from a five-minute one. A Postgres stall degrades the dashboard; it does not stop collection.

Accepting a hit is a parse, a classify, an enrich and a push into an in-memory queue. The writer runs one lane per table, each with its own buffer, so a pathological replay payload cannot delay a pageview and a failing insert on one table cannot stall another.

The honest limits

A single VPS is the shape of this deployment, and there are things it is not.

There is no cluster
No multi-node deployment, no sharded ClickHouse, no horizontal scale-out, and nothing in the compose file pretending otherwise. The API and ingest paths are stateless apart from the writer's in-memory queue, so more than one server against shared stores is architecturally plausible. It has not been built or tested, and this page is not going to describe a deployment nobody has run.
Replay is the disk outlier
One recorded session outweighs a month of events, which is why it has a retention clock of its own, defaulting to thirty days. Events default to keeping everything.
No bytes-per-event figure is offered
This project has not measured one, so quoting one would be an invention. Two weeks of your own traffic, read from the usage endpoint and the ClickHouse volume, is a better forecast than anyone else's benchmark.
Mail is optional, and degrades rather than fails
With no SMTP URL, invitations become links you copy by hand, password resets become links an admin hands over, and scheduled reports do not run. Nothing else changes.
The store images are not pinned for you
The defaults use latest-alpine, which is convenient for a first run and is not a version. Pin them in .env before you depend on the install.
Backups are yours
./backup.sh exists and works. Nobody is running it for you, and nobody is watching the disk.
It is pre-release
There is no tagged release, no production install anywhere, and no benchmark. If that is not an acceptable trade for the reporting, it is the right decision to wait.
Close registration after your first account

An open signup form on a reachable dashboard is how a self-hosted install acquires strangers. Create your account, then set MICAFORGE_DISABLE_REGISTRATION=true and bring the server back up.

Operating it

Watch what it is doing
docker compose logs -f micaforge
Update to a newer build
./update.sh
Take a backup
./backup.sh
Stop it without losing anything
docker compose down, which keeps every volume.