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:
- The built-in default. What you get by setting nothing.
- The environment —
.env, or the shell that starts the server. - 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.
| Setting | What 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
| Setting | Default | What 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
| Setting | What 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
| Setting | What it does | If 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
| Setting | Default | What 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
| Setting | Default | What 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.
| Setting | Default | What it does |
|---|---|---|
SMTP_HOST | unset | Unset means no mail: codes go to the log instead. |
SMTP_PORT | 587 | 587 for STARTTLS, 465 for implicit TLS. |
SMTP_SECURE | false | true for implicit TLS on 465. |
SMTP_USER · SMTP_PASS | unset | Credentials, if your provider wants them. |
MAIL_FROM | Kern <no-reply@localhost> | The From address. Use a domain with SPF and DKIM set up, or mail lands in spam. |
INBOUND_EMAIL_SECRET | unset | Shared secret the inbound email worker sends. Empty turns email-to-stream off, and the app stops offering addresses. |
INBOUND_EMAIL_DOMAIN | unset | The 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
| Setting | Default | What it does |
|---|---|---|
APP_SCHEME | kern | The URL scheme the desktop app registers, used by invite links. |
DOWNLOAD_URL | the public release page | Where an invite link sends somebody who hasn't got the app yet. |
STREAMS_DOCS_URL | unset | Where your copy of the stream documentation lives. Linked from rejection responses. |
EXTRA_CORS_ORIGINS | unset | Extra 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.
| Setting | Default | What it caps |
|---|---|---|
INVITE_EXPIRY_DAYS | 14 | How long an invitation stays usable. |
WORKSPACES_PER_USER | 25 | Workspaces one account may own. |
MEMBERS_PER_WORKSPACE | 200 | People in one workspace. |
LOGO_MAX_BYTES | 307200 | Workspace logos and avatars (small data URLs, resized by the app). |
ASSET_MAX_BYTES | 3145728 | One shared whiteboard image. Your proxy's body limit must be at least this. |
ASSET_QUOTA_MB | 1024 | Total whiteboard images per space. The setting that decides how fast your disk fills. |
STREAMS_PER_SPACE | 25 | Streams one space may have. |
STREAM_MAX_BODY_BYTES | 262144 | One stream submission. |
STREAM_MAX_PER_HOUR | 1000 | Hourly ceiling per stream, above each stream's own rate limits. |
STREAM_LOG_MAX_DAYS | 90 | Longest delivery-log retention a stream may ask for. |
STREAM_EMAIL_PER_HOUR | 120 | Inbound messages per hour for an email stream. |
SHARES_ENABLED | true | false turns off public share links and calendar feeds entirely. |
SHARE_MAX_CONTENT_BYTES | 524288 | A published note or project snapshot. |
SHARE_MAX_IMAGE_BYTES | 4194304 | A published whiteboard image. |
SHARES_PER_USER | 200 | Live 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.