Configuration
Every variable the server reads, what it defaults to, and what happens when it is unset.
.env.example in the repository is the complete inventory: every variable, once, with what
reads it. If a variable is not in that file, nothing reads it. This page is the tour.
docker compose reads .env automatically. So does cargo run, through dotenvy. Keep it
out of version control: it holds the key every other key is derived from.
Required
MICAFORGE_SECRET_KEY_BASE= # openssl rand -base64 48
POSTGRES_PASSWORD= # openssl rand -hex 24
CLICKHOUSE_PASSWORD= # openssl rand -hex 24
MICAFORGE_DOMAIN=:80
ACME_EMAIL=
MICAFORGE_SECRET_KEY_BASE is the one operator secret. Session cookies, sealed
credentials, share-link signatures and the ingest HMAC are all derived from it. Rotating it
logs everyone out and makes stored secrets unreadable. 32 characters minimum, refused at
boot below that.
MICAFORGE_DOMAIN is read by Caddy. A hostname turns on automatic HTTPS; the literal :80
serves plain HTTP, which is what you want on a laptop or behind a proxy that terminates TLS
already.
Keep the two store passwords to characters that need no percent-encoding: they are interpolated into connection URLs. Hex is the safe choice and is what the installer generates.
Public addresses
MICAFORGE_PUBLIC_URL=https://analytics.example.com
MICAFORGE_DASHBOARD_URL=https://analytics.example.com
Both must be addresses a browser can reach, not the address the server binds. They are printed into invitation emails, share links and the tracking snippet, so a wrong value here is a snippet that silently sends nothing. They are two variables because a managed install can serve the dashboard from a CDN and the API from elsewhere.
Stores
In the shipped compose file the three connection URLs are assembled from the parts and the
container names, so there is nothing to set beyond the passwords. Spelled out, for a local
cargo run:
MICAFORGE_DATABASE_URL=postgres://micaforge:PASSWORD@127.0.0.1:5432/micaforge
MICAFORGE_CLICKHOUSE_URL=http://127.0.0.1:8123
MICAFORGE_CLICKHOUSE_DB=micaforge
MICAFORGE_CLICKHOUSE_USER=micaforge
MICAFORGE_CLICKHOUSE_PASSWORD=
MICAFORGE_REDIS_URL=redis://127.0.0.1:6379
ClickHouse takes its database and credentials separately rather than folded into the URL,
because the driver does. Valkey speaks the Redis wire protocol, so the scheme stays
redis://.
Postgres memory is tuned for a 4 GB machine shared with ClickHouse:
POSTGRES_SHARED_BUFFERS, POSTGRES_EFFECTIVE_CACHE_SIZE, POSTGRES_WORK_MEM,
POSTGRES_MAX_CONNECTIONS. On a bigger box, raise shared buffers to about a quarter of RAM
and effective cache size to about half.
Behaviour
MICAFORGE_BIND_ADDR=0.0.0.0:8080
MICAFORGE_DISABLE_REGISTRATION=true
MICAFORGE_SELF_HOSTED=true
MICAFORGE_LOG_FORMAT=json
MICAFORGE_WORKER_CONCURRENCY=2
MICAFORGE_WRITER_BUFFER_MB=32
RUST_LOG=info
Registration is closed by default. The first account on an install is always allowed and
becomes its owner; everyone after it joins by invitation, unless you set
MICAFORGE_DISABLE_REGISTRATION=false.
MICAFORGE_WRITER_BUFFER_MB is how much each event table may hold in memory while
ClickHouse is away, for a restart or an upgrade. The server keeps retrying and loses nothing
until the buffer is full; then it drops the oldest rows and logs how many. Six tables, so the
worst case is six times the value.
MICAFORGE_SELF_HOSTED gates no feature, because there are none to gate. It selects the
admin surface and guarantees no outbound call at runtime.
MICAFORGE_WORKER_CONCURRENCY is how many background jobs run at once: crawls, imports,
scheduled reports, alert evaluation. Two is right for a 4 GB VPS, where the ingest path must
not queue behind an import. It defaults to half the cores, minimum 2, maximum 16.
Retention
MICAFORGE_RETENTION_DAYS=0
MICAFORGE_REPLAY_RETENTION_DAYS=30
0 means forever, and forever is the default: an analytics tool that quietly deleted
history because a variable was unset would be indefensible. See
data retention.
Geography, off by default
Micaforge makes no outbound call to resolve an address, so geography needs a database on
disk. Drop a MaxMind-compatible .mmdb into infra/geoip/ and point these at the path
inside the container:
MICAFORGE_GEOIP_CITY_PATH=/geoip/GeoLite2-City.mmdb
MICAFORGE_GEOIP_ASN_PATH=/geoip/GeoLite2-ASN.mmdb
Behind Cloudflare, with MICAFORGE_TRUSTED_PROXY_HEADER=cf-connecting-ip, an install with
no city database takes the visitor’s country from Cloudflare’s CF-IPCountry header, and
the region, city and coordinates too when the zone has Cloudflare’s “Add visitor location
headers” managed transform turned on. The same trust that names the visitor’s address names
their location; the database, when there is one, still wins.
Without either, you get no geography at all. Without the ASN database there is no datacenter detection, which weakens agent verification without breaking it, since user-agent patterns and reverse DNS still apply.
Mail, off by default
MICAFORGE_SMTP_URL=smtp://user:password@smtp.example.com:587
MICAFORGE_MAIL_FROM=Micaforge <noreply@example.com>
With no SMTP URL every email path degrades instead of failing: invitations become links you copy from the dashboard, scheduled reports do not run, and a password reset is a link the operator prints on the host:
docker compose exec micaforge micaforge-server reset-link you@example.com
The link works once and expires in an hour. No link or token is ever written to the log, because anyone who can read a log could otherwise take over the account.
Images and ports
MICAFORGE_IMAGE=micaforge # or ghcr.io/micaforge/micaforge
MICAFORGE_WEB_IMAGE=micaforge-web # or ghcr.io/micaforge/micaforge-web
MICAFORGE_VERSION=local # or a release, such as 0.1.0
POSTGRES_IMAGE=postgres:17-alpine
CLICKHOUSE_IMAGE=clickhouse/clickhouse-server:26.8-alpine
VALKEY_IMAGE=valkey/valkey:8-alpine
HTTP_PORT=80
HTTPS_PORT=443
With bare image names the server and web images are built from your checkout. With a
registry path they are pulled, at MICAFORGE_VERSION. ./install.sh --version 0.1.0 sets
all three for you.
The stores are pinned to a major version (Postgres, Valkey) or an LTS line (ClickHouse), so
a pull brings patch releases and never a format change. ClickHouse cannot be downgraded
once it has written data: move that line on purpose, after ./backup.sh.
Moving HTTP_PORT off 80 breaks certificate issuance: ACME’s HTTP-01 challenge needs port
80 reachable on the standard port.
Who the visitor is
MICAFORGE_TRUSTED_PROXIES= # read by Caddy
MICAFORGE_TRUSTED_PROXY_HEADER=x-real-ip # read by the server; set in docker-compose.yml
The visitor address feeds the daily visitor hash and the rate limits, and is never stored.
Caddy writes the address it resolved into X-Real-IP, overwriting any a client sent, and
the server believes that header and nothing else.
MICAFORGE_TRUSTED_PROXIES is who Caddy believes when they say who the visitor is. Empty
trusts nobody, which is right when Caddy faces the internet. Behind your own proxy on the
same network, set private_ranges; behind a CDN, list the CDN’s published ranges, space
separated. Get it wrong and every visitor arrives with the proxy’s address, so they all
count as one visitor and share one rate-limit bucket.
Hardening and limits
MICAFORGE_ADMIN_EMAILS= # extra instance admins, comma separated
MICAFORGE_RATE_LIMIT_PER_MIN=3000 # per client address, 0 switches it off
MICAFORGE_CORS_ORIGINS= # extra dashboard origins, comma separated
MICAFORGE_ALLOW_PRIVATE_OUTBOUND=false # let webhooks reach your own network
The first registered account is always an instance admin. An address in
MICAFORGE_ADMIN_EMAILS becomes one only after its owner verifies it.
Outbound requests (robots.txt and sitemap reads, webhooks, imports by URL) refuse private,
loopback and link-local addresses, after DNS and on every redirect, so a site record cannot
be pointed at your database or a cloud metadata endpoint. Turn
MICAFORGE_ALLOW_PRIVATE_OUTBOUND on only if your webhooks really go to a host on your own
network.
The web app
The shipped web image needs no build-time configuration: the dashboard calls the API on whatever address served it. Two Astro variables exist for other setups, and both are read at build time:
PUBLIC_API_URL=https://api.example.com # only when the dashboard is on another origin
PUBLIC_SITE_URL=https://micaforge.com # canonical address of the marketing pages