Configuration

A self-hosted Kern server is configured entirely with environment variables, normally in a .env file next to docker-compose.yml. Only four have to be set. Everything on this page has a working default.

How settings work

The shipped .env.example documents each variable next to its default; copy it to .env and edit. The installer writes the file for you with the secrets already generated.

A value can come from three places, and a later one wins:

  1. The built-in default. What you get by setting nothing.
  2. The environment — .env, or the shell that starts the server.
  3. A live override, for the handful of settings that can be changed while the server is running. See live settings.

To see where a value is actually coming from, on the server:

kernctl settings

Changing anything in .env needs a restart (kernctl service restart, or docker compose up -d). Changing a live setting does not.

The four required

A server will not start usefully without these. The installer generates the first three.

SettingWhat to put in it
POSTGRES_PASSWORD A long random string. docker compose builds DATABASE_URL from it. openssl rand -base64 36
BETTER_AUTH_SECRET At least 32 random characters; it signs sessions. Changing it signs everyone out. openssl rand -base64 48
STREAMS_ENCRYPTION_KEY 32 random bytes, base64. Encrypts stream credentials. openssl rand -base64 32
PUBLIC_URL The address people and apps use to reach this server, as they see it.

Back up STREAMS_ENCRYPTION_KEY with your database. If it is unset, a key is derived from BETTER_AUTH_SECRET instead — which means rotating that secret silently makes every stored stream secret, GitHub token and Turnstile key unreadable. Setting it explicitly decouples the two, so you can rotate sessions without losing stream credentials.

Addresses

SettingDefaultWhat it does
PUBLIC_URL http://localhost:8787 The origin the server believes it is reachable at. Invite links, share links and stream endpoints are built from it, so behind a reverse proxy this must be the public https:// origin, not the loopback address the process listens on.
LINK_URL Follows PUBLIC_URL Set only if invite and share links are served from their own hostname.
STREAMS_URL Follows PUBLIC_URL Set only if stream endpoints are served from their own hostname.
INSTANCE_NAME Kern Self-Host What this server calls itself: in emails, on invite pages, on the app's sign-in screen and in Kern Platform.
SUPPORT_EMAIL unset Who people should contact about this server. Shown in emails.

A single-origin server — one hostname, everything under it — wants PUBLIC_URL set and the other two left empty. That is the shape the defaults assume and the one that needs the least from your proxy.

kernctl health compares PUBLIC_URL against what the proxy actually serves, because getting it wrong is quiet: sync works, and then an invitation email sends someone to an address that doesn't resolve.

Database

SettingWhat it does
POSTGRES_PASSWORD Used by docker compose to create the database user and to build DATABASE_URL.
DATABASE_URL Set this yourself when running the API outside compose, e.g. postgres://kern:…@localhost:5432/kern. PostgreSQL 15 or newer.

Migrations run at startup, every start, and are idempotent — there is no separate migration step and nothing to remember during an upgrade.

Secrets

SettingWhat it doesIf you change it
BETTER_AUTH_SECRET Signs sessions. Everyone is signed out and signs in again. Nothing is lost.
STREAMS_ENCRYPTION_KEY AES-256-GCM over stream secrets, GitHub tokens and Turnstile keys. Stored stream credentials become unreadable. Streams need new secrets and tokens. Don't change it casually, and back it up.

Both belong in your backups, not just the database. See backups.

Who may join

SettingDefaultWhat it does
REGISTRATION invite open: anyone who can reach the server may sign up. invite: only an address holding a workspace invitation, or one an administrator added with kernctl allow add. closed: nobody new. Live — changeable without a restart.
ALLOWED_EMAIL_DOMAINS unset With REGISTRATION=open, limits sign-ups to these domains, e.g. example.com,example.org. The usual shape for a company server that shouldn't need invitations internally.
MAX_USERS 0 How many accounts the instance will hold. 0 means no limit.

The very first account is always allowed, whatever these say, so a server you have just installed is usable without first editing anything. Create your own account before you open the address to anybody else.

Management

SettingDefaultWhat it does
ADMIN_LOCAL_ONLY true The management API answers only the server's own machine. Nothing on the network can even attempt a token. Manage it over loopback or an SSH tunnel.
ADMIN_ALLOW_FROM unset Read only when ADMIN_LOCAL_ONLY=false. Addresses or IPv4 CIDR ranges, e.g. 192.168.1.0/24. With the gate open and no list, nothing is allowed.
ADMIN_TOKEN_FILE /var/lib/kern/admin-token Where the first management token is written on first start. kernctl reads it from here, which is why it needs no setup on the server itself. Keep it readable only by the service account.
ADMIN_LOG_LINES 2000 Lines of server log kept in memory for kernctl logs and Kern Platform. A restart clears it; the process log keeps history.

The client address is taken from the socket, not from a header. A proxy header is attacker-controlled, and trusting one here would undo the gate completely. So if you put the management API behind a reverse proxy, the proxy is what the allow list sees, and you must restrict who reaches it there instead.

Mail

SettingDefaultWhat it does
SMTP_HOSTunsetUnset means no mail: codes go to the log instead.
SMTP_PORT587587 for STARTTLS, 465 for implicit TLS.
SMTP_SECUREfalsetrue for implicit TLS on 465.
SMTP_USER · SMTP_PASSunsetCredentials, if your provider wants them.
MAIL_FROMKern <no-reply@localhost>The From address. Use a domain with SPF and DKIM set up, or mail lands in spam.
INBOUND_EMAIL_SECRETunsetShared secret the inbound email worker sends. Empty turns email-to-stream off, and the app stops offering addresses.
INBOUND_EMAIL_DOMAINunsetThe domain inbound stream addresses use.

Any SMTP provider works. Without mail, a server is still perfectly usable by one person — read the code out of the log, or run kernctl users verify <email> — but nobody else can finish signing up, and the app's sign-in screen says so. /instance reports features.email, so apps know before they try.

The app

SettingDefaultWhat it does
APP_SCHEMEkernThe URL scheme the desktop app registers, used by invite links.
DOWNLOAD_URLthe public release pageWhere an invite link sends somebody who hasn't got the app yet.
STREAMS_DOCS_URLunsetWhere your copy of the stream documentation lives. Linked from rejection responses.
EXTRA_CORS_ORIGINSunsetExtra browser origins allowed to call the API. The desktop app's own origins are always allowed; add one only if you build your own client.

Limits and quotas

All optional, all with sensible defaults. Raise them if your instance needs it; lower them if you are handing the address to people you don't know.

SettingDefaultWhat it caps
INVITE_EXPIRY_DAYS14How long an invitation stays usable.
WORKSPACES_PER_USER25Workspaces one account may own.
MEMBERS_PER_WORKSPACE200People in one workspace.
LOGO_MAX_BYTES307200Workspace logos and avatars (small data URLs, resized by the app).
ASSET_MAX_BYTES3145728One shared whiteboard image. Your proxy's body limit must be at least this.
ASSET_QUOTA_MB1024Total whiteboard images per space. The setting that decides how fast your disk fills.
STREAMS_PER_SPACE25Streams one space may have.
STREAM_MAX_BODY_BYTES262144One stream submission.
STREAM_MAX_PER_HOUR1000Hourly ceiling per stream, above each stream's own rate limits.
STREAM_LOG_MAX_DAYS90Longest delivery-log retention a stream may ask for.
STREAM_EMAIL_PER_HOUR120Inbound messages per hour for an email stream.
SHARES_ENABLEDtruefalse turns off public share links and calendar feeds entirely.
SHARE_MAX_CONTENT_BYTES524288A published note or project snapshot.
SHARE_MAX_IMAGE_BYTES4194304A published whiteboard image.
SHARES_PER_USER200Live share links one account may hold.

If a limit is doing something you didn't expect, kernctl settings shows the value in force, and kernctl health reports table sizes and unused whiteboard images so you can see what is actually growing.

Live settings

A few settings can be changed while the server runs, because they decide what the app offers rather than how the process is built. The app sees the difference on its next sign-in screen, with no restart and nobody disconnected.

kernctl settings                     # every setting, and where it comes from
kernctl set registration open        # change one
kernctl set registration default     # fall back to .env

Registration mode, the instance notice and whether publishing is on are the live ones. Kern Platform changes the same values in a window. default removes the live override so the .env value applies again.

Everything else — secrets, addresses, the database, mail, limits — is read at startup, so it needs a restart.

Worked examples

Just me, on one machine

No hostname, no certificate, no mail. Reach it over an SSH tunnel and let the one verification code come out of the log.

PUBLIC_URL=http://localhost:8787
REGISTRATION=closed
POSTGRES_PASSWORD=…
BETTER_AUTH_SECRET=…
STREAMS_ENCRYPTION_KEY=…

Create your account first, then set closed — or leave the default invite, which has the same practical effect.

A team, on a company domain

Anyone with a company address may sign up, mail works, and the management API stays on loopback.

PUBLIC_URL=https://kern.example.com
INSTANCE_NAME=Acme Labs
SUPPORT_EMAIL=it@example.com
REGISTRATION=open
ALLOWED_EMAIL_DOMAINS=example.com
SMTP_HOST=smtp.example.com
SMTP_USER=kern@example.com
SMTP_PASS=…
MAIL_FROM=Acme Kern <no-reply@example.com>

Invitation-only, for a small group

The default shape. People get in because somebody invited them, or because you did.

PUBLIC_URL=https://kern.example.com
REGISTRATION=invite
MAX_USERS=50
SMTP_HOST=…
kernctl allow add alice@example.com "new starter"

Tight on disk

Whiteboard images are what grow. Cap them per space and keep the stream log short.

ASSET_QUOTA_MB=256
ASSET_MAX_BYTES=1048576
STREAM_LOG_MAX_DAYS=14

kernctl health --fix then removes unused images and prunes the log for you; see keeping it healthy.