Meetnix

Self-host

An hour, one box, and a domain you already own.

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

Four things, and one of them is optional.

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.

A name and a certificate

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.

An identity provider

Any OIDC issuer — Entra, Okta, Keycloak, Authentik, Kanidm, Google Workspace. One client, one redirect URI, one secret in a file.

A GPU, optionally

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.

1 public UDP port, all media multiplexed onto it
:443 the only TCP port, and the app and its API share one origin
0 outbound connections required after install
0 secrets read from an image, a repo or the Nix store

02 — NixOS

The reference deployment.

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.

  • Secrets are paths. oidc.clientSecretFile, not oidc.clientSecret — the module refuses to read a secret out of the store, where it would be world-readable.
  • Branding is an option. Your name on the tab, the sign-in card and the outgoing mail, without a fork and without a paid tier.
  • The config on the right is real. It is evaluated on every build of this website, so it cannot drift into being an example that no longer works.
configuration.nix
{
  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";
    };
  };
}
compose.yaml
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

The one to try first.

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.

  • Static binaries. The Go services are built with CGO_ENABLED=0, so the image is small and has nothing in it to patch on a Tuesday.
  • Recordings are a bind mount. Point it at the volume you already back up; the archive is a directory tree and nothing else.
  • Air-gapped works. Pull the image once, carry it in. Nothing phones home, and no feature quietly stops working without a licence check.

04 — Kubernetes

When one box stops being the answer.

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.

  • S3-compatible storage. Point it at MinIO, Ceph, or a bucket — content-addressed, with the manifest written last so a half-uploaded meeting is never mistaken for a complete one.
  • Regional pinning. Schedule the SFU where the participants are and the recorder where the data is allowed to be. They do not have to be the same place.
  • ServiceMonitor included. Metrics, traces and a dashboard in the chart, so "is it up" has an answer before you need one.
values.yaml
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

What it actually costs to keep the recordings.

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
Video is the expensive part and it is rarely the useful part. The default policy prunes it at thirty days and keeps audio, transcript and manifest — which is the half you would actually go back to, and it fits on a disk you already own. Free-space pressure prunes video early rather than failing a recording, so the failure mode of a full disk is a shorter video history, not a lost meeting.

Compared to per seat, per month

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 add

06 — Identity

The sign-in is not a feature. It is the attribution.

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.

Register one client

Confidential client, authorisation code with PKCE, redirect to /portal/callback. Scopes: openid, profile, email.

Map groups to rooms

A directory group becomes a room's membership. Joiners and leavers are handled where they already are, not in a second admin panel.

Invite outsiders explicitly

Signed, expiring, single-room invites for people without an account. A guest cannot see the archive and cannot mint further invites.

Everything else follows

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

The boring parts, written down.

Backup is a directory

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.

Upgrades are atomic

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.

Monitoring

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.

Hardened by default

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.

Diagnostics for users

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.

Leaving is supported

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.

Stuck on step three?

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.