A host
4 vCPU and 8 GB is comfortable for a 25-person company. The SFU is network-bound, not CPU-bound — there is no transcoding, because there is no compositing.
Self-host
Meetnix ships the media server, the sign-in bridge, the recorder, the archive and the TLS front door as one unit. Pick the shape that matches your estate; all three run the same binaries and write the same archive.
docker run -p 8443:8443 ghcr.io/meetnix/meetnix
try it in a minute
01 — Before you start
4 vCPU and 8 GB is comfortable for a 25-person company. The SFU is network-bound, not CPU-bound — there is no transcoding, because there is no compositing.
One A/AAAA record. ACME is wired in on NixOS; bring your own PEM anywhere else. Browsers will not hand over a camera without TLS.
Any OIDC issuer — Entra, Okta, Keycloak, Authentik, Kanidm, Google Workspace. One client, one redirect URI, one secret in a file.
For transcription. It can be a different machine, and one worker can serve several deployments. Skip it and you still get recordings — just no transcript.
02 — NixOS
This is how we run it, so it is the path that gets tested first and breaks last. The module composes the portal, the SFU, the recorder and nginx, opens exactly the ports it needs, and boots inside a systemd sandbox that a VM test asserts is not accidentally permissive.
oidc.clientSecretFile, not oidc.clientSecret — the module refuses to read a secret out of the store, where it would be world-readable.
{
inputs.meetnix.url = "github:meetnix/meetnix";
# …then, in the host's config:
imports = [ meetnix.nixosModules.default ];
services.meetnix = {
enable = true;
domain = "call.acme.com";
useACMEHost = "call.acme.com";
operators = [ "alex@acme.com" ];
oidc.issuer =
"https://sso.acme.com/realms/staff";
oidc.clientSecretFile = "/run/secrets/oidc";
sessionKeyFile = "/run/secrets/session";
livekitKeyFile = "/run/secrets/livekit";
recording = {
directory = "/srv/meetnix";
freeSpaceFloorGiB = 20;
retention = {
video = "30d"; audio = "2y";
};
};
branding = {
organisation = "Acme";
supportURL = "https://meetnix.ai/docs";
};
};
}
services:
meetnix:
image: ghcr.io/meetnix/meetnix:1.0
restart: unless-stopped
ports:
- "443:8443"
- "21851:21851/udp"
environment:
MEETNIX_DOMAIN: call.acme.com
MEETNIX_OIDC_ISSUER: https://sso.acme.com
MEETNIX_OIDC_CLIENT_ID: meetnix
MEETNIX_RETENTION_VIDEO: 30d
secrets:
- oidc_client_secret
- session_key
volumes:
- ./recordings:/var/lib/meetnix
- ./tls:/etc/meetnix/tls:ro
secrets:
oidc_client_secret: { file: ./secrets/oidc }
session_key: { file: ./secrets/session }
03 — Docker
One image with the portal, the SFU and the recorder inside it.
docker compose up gets you a working instance with a
self-signed certificate in about a minute — enough to have a
real call with a colleague and look at what it wrote.
CGO_ENABLED=0, so the image is small and has nothing in it to patch on a Tuesday.
04 — Kubernetes
The portal is stateless and scales horizontally. The SFU scales by replica with a node port per replica, because media wants a real address rather than an ingress. Recorders are jobs, spawned per room and reaped when it ends.
domain: call.acme.com
portal:
replicas: 2
sfu:
replicas: 3
nodePortRange: "21851-21853/udp"
oidc:
issuer: https://sso.acme.com/realms/staff
existingSecret: meetnix-oidc
storage:
class: s3
endpoint: https://minio.acme.internal
bucket: meetnix-recordings
region: eu-central
transcription:
enabled: true
nodeSelector: { gpu: "true" }
metrics:
serviceMonitor: true
05 — Sizing
Per-track capture writes more bytes than a mixdown — that is the honest trade, and it is smaller than it sounds because there is no re-encode and video prunes first. Numbers for a 25-person company doing fifteen hours of meetings a week.
| What | Rate | One hour, 6 people | A year, 15 h/week |
|---|---|---|---|
| Audio, per speaker | ~32 kbit/s Opus | ~86 MB | ~67 GB |
| Video, per camera | ~1.2 Mbit/s VP8 | ~3.2 GB | pruned at 30 days |
| Transcript and manifest | text | ~180 kB | ~140 MB |
| Bandwidth at the server | n × 1.5 Mbit/s up | ~9 Mbit/s | — |
| Keep audio + transcript forever | the default | — | ≈ 67 GB/year — about one disk, ever |
Twenty-five seats of hosted conferencing with recording is roughly $6,000 a year, every year, and the recordings stay with the vendor. The same workload on a $40/month box is $480 a year, the storage is a disk, and the archive is yours whatever happens to us. That gap is not a discount — it is the entire reason this category exists.
What the paid tiers add06 — Identity
Meetnix has no user database. The name in the manifest is the name in the token your issuer signed, which is why there is nothing to spoof and nothing to reconcile — and why "who was in this meeting" is answerable months later, after somebody has left.
Confidential client, authorisation code with PKCE, redirect to
/portal/callback. Scopes: openid,
profile, email.
A directory group becomes a room's membership. Joiners and leavers are handled where they already are, not in a second admin panel.
Signed, expiring, single-room invites for people without an account. A guest cannot see the archive and cannot mint further invites.
Tokens are minted per room per session, the SFU never sees an identity it was not handed, and the manifest records which issuer vouched for each person.
Tested against Keycloak, Authentik, Kanidm, Entra ID, Okta and Google Workspace. If your issuer is standards-compliant it will work; if it is not, tell us which one and we will make it work — hello@meetnix.ai.
07 — Keeping it running
Copy recordings/ and your secrets. There is no
database to dump — the manifest is the source of truth and the
search index rebuilds from disk.
On NixOS, a rollback is a generation. Elsewhere, a tag. The archive format is versioned and forward-compatible, so a newer build always reads an older archive.
Prometheus metrics for rooms, tracks, recorder health, transcript queue depth and free space. Alert on the free-space floor before it starts pruning for you.
Services run under a systemd sandbox with a deny-by-default address family and no write access outside their own state. A VM test boots a real machine and asserts it.
A page that tells someone why their camera did not start, in words, with your support link on it — so the answer to a device problem is not a ticket.
Export the whole archive as it sits on disk. It is media files and JSON, it needs none of our software to read, and that is deliberate.
We would rather help you self-host than sell you the hosted version. An instance that works is worth more to us than a trial that stalls.