Gryt
Host Gryt

The gryt CLI

gryt, the terminal manager for self-hosted Gryt servers

gryt creates and runs self-hosted Gryt servers from a terminal. Run it with no arguments and it opens a keyboard-driven manager: create a server profile, edit its settings with validation, and start, stop or inspect the Docker deployment it writes for you.

Install

curl -fsSL https://get.gryt.chat | sh

The script picks the build for your platform, checks it against the release checksums, and installs to /usr/local/bin if that's writable or ~/.local/bin if not. Two variables change what it does:

VariableEffect
GRYT_VERSIONInstall a specific tag, such as v0.1.0, instead of the newest release
GRYT_INSTALL_DIRInstall somewhere other than the default

The script doesn't cover Windows. Download the .zip from the releases page instead.

From source, with Go 1.25 or newer:

go install github.com/Gryt-chat/cli/cmd/gryt@latest

Starting a deployment needs Docker Desktop, or Docker Engine with the Compose plugin. Creating profiles and writing their files works without Docker.

Updating

gryt update

That replaces the binary in place, checking the download against the release checksums first. gryt update --check reports whether a newer release exists and changes nothing.

The manager also checks once when it starts. If there's a newer release it says so above the server list, and u applies it. The check asks GitHub which release is newest and sends nothing else, no identifier and nothing recorded. It fails silently, so a machine with no route out shows its servers rather than an error.

gryt version prints what you have.

Re-running the installer does the same job, and is the way back if the binary lives somewhere you can't write. gryt update says so rather than asking for a password:

curl -fsSL https://get.gryt.chat | sh

To pin a version or roll one back, install it explicitly. gryt update always moves to the newest release, so it's the installer that goes backwards:

GRYT_VERSION=v0.1.2 sh -c "$(curl -fsSL https://get.gryt.chat)"

Note the shape. GRYT_VERSION=v0.1.2 curl -fsSL https://get.gryt.chat | sh sets the variable for curl rather than for the shell running the script, so it's ignored and you get the newest release anyway, without being told.

Release channels

Stable is the default. gryt channel beta switches this machine to beta:

gryt channel beta      # follow beta
gryt channel           # print the channel this machine follows
gryt channel stable    # go back

The channel covers two things at once. gryt update follows it, so the CLI itself moves to beta builds. Servers you start also run the latest-beta images rather than latest, for the server, the SFU and the image worker.

Existing servers keep the image they already pulled until you restart them, so switching the channel changes nothing that's already up. Restart a server to move it.

Features

  • Live dashboard: every server on one screen, refreshing itself, with the address you hand people rather than the address it binds to
  • A wizard that asks answerable questions: which of this machine's addresses people will reach it on, not what a WebSocket URL should be
  • Voice and uploads without configuration: one SFU and one object store per machine, shared by every server on it
  • Ports that don't collide: it offers the first port nothing else holds
  • gryt doctor: says what is wrong with Docker, the config directory, or a port something else has taken
  • Updates itself: gryt update, or u when the dashboard says one is available
  • Private by default: profiles and generated .env files are readable only by you

Using the manager

gryt

Every server, its live state, and the address to hand out:

gryt  server manager                          3 servers · voice ready
  NAME                         STATUS     ADDRESS               VOICE    UPLOADS

  Archive                      ○ stopped  192.168.1.42:5003   —        here
› Pivert CLI Server            ● running  192.168.1.42:5001   ready    here
  Staging                      ○ stopped  192.168.1.42:5002   —        here

  SHARED  one of each, used by every server above
  Voice (SFU)                  ● running  carries voice for every server here
  Object store                 ● running  holds uploads for every server here

The two rows under SHARED are the pieces this machine runs one of. l follows their output, which is where to look when voice stops working. x on one of them stops it for every server at once, and says so.

An image worker runs beside each server rather than being shared, because it reads that server's own database. It has no row: l on the server already carries its output too.

It refreshes every few seconds on its own, so a server that stops says so without you asking. l follows a server's log rather than showing a snapshot.

KeyAction
or k jSelect a server
enterOne server, with its addresses grouped by who they're for
nNew server
eEdit the selected server
cChange the settings the server keeps in its own database
s x rStart · stop · restart
lFollow the log
gRefresh now
uUpdate, when one is available
qQuit

Every key that acts on a server works inside enter too, so you can start, stop and read a log without going back out.

Which address to give people

The ADDRESS column is the address other machines on your network can use. enter groups them by who each one is for:

  On your network
  192.168.1.42:5001   en0, local network

  From the internet
  203.0.113.10:5001
  only if port 5001 is forwarded to this machine

  This machine only
  127.0.0.1:5001

The public address can't be read off this machine's interfaces. Behind NAT the machine holds a private address and has no way of knowing what the world sees, so something outside has to answer. The CLI asks a STUN server, which is what STUN is for and what the SFU already uses for voice: one UDP packet to stun.l.google.com:19302, which replies with the address it saw. Nothing else is sent, and nothing is recorded.

It runs once, the first time you open a server with enter, and never on the table. A machine that already holds a public address on an interface is answered from the interface without any packet leaving. Set GRYT_NO_PUBLIC_LOOKUP=1 to skip it entirely; the line then reads "lookup turned off".

The caveat under the address is the important part. A public address doesn't make a server reachable on its own: the port has to be forwarded to this machine first.

The wizard

n for a new server, e to edit one. enter advances and saves on the last step, shift+tab goes back, esc cancels.

Eight questions, and the count moves as you answer: ticking "a domain" adds one, choosing external S3 adds six.

StepQuestionDefault
1Server namenone, required
2Bind address0.0.0.0, reachable from other machines
3Portthe first one nothing else on this machine holds
4Security levelbalanced
5Voice seats0, no limit
6Trusted proxy hops0
7Where will people connect fromthis machine, ticked
8Where do uploads goshared

Every default is a placeholder rather than text you have to delete: type and it replaces, leave it and you get the default.

Where will people connect from

Step 7 is a tick-list of this machine's own addresses, read from its interfaces. Docker bridges and other virtual interfaces are left out, because advertising those hands clients candidates that can never connect.

Where will people connect from?
Space ticks, ↑/↓ moves. Tick every route that applies.

  › [x] This machine only (localhost)
    [ ] 192.168.1.42  (en0, local network)
    [ ] A domain or address I will type

Tick every route that applies. Clients are given all of them and use whichever answers fastest, so a server reachable over a LAN and over the internet is both. Ticking the last option adds a step to type a domain, for a server behind a reverse proxy with TLS.

Where do uploads go

AnswerWhat happens
sharedThe object store on this machine, with thumbnails and compression. Needs nothing from you
filesystemStraight into the server's own folder. No extra containers, no thumbnails
s3A storage service you already have. Adds six steps for the endpoint, bucket, region, keys and path-style setting

Security levels

Every level is invite-only. What changes between them is who may hold an identity on the server, and whether the server advertises itself over mDNS on the local network.

LevelIdentitiesLAN discovery
strictGryt accounts onlyNot advertised
balancedGryt accounts onlyAdvertised
communityGryt accounts and local identitiesAdvertised

A Gryt account is an identity from auth.gryt.chat that works across servers. A local identity is a keypair generated on the device, which never leaves it and is known only to this server. Allowing local identities means somebody can join without an account anywhere.

LAN discovery is mDNS advertising, so a Gryt client on the same network lists the server without being given its address. Turning it off stops the advertisement; it isn't an access control, since an invite still works either way.

Voice seats

0, the default, means no cap, which is what the server does on its own.

A cap is about the machine running the SFU rather than about ports. Every participant's media shares one UDP port, so there's no per-person port to run out of. What runs out first is CPU and upload bandwidth.

Settings a server keeps itself

Some of a server's configuration lives in its own database rather than in the environment its container was started with: who may join, whether it advertises itself over mDNS, and what it does about profanity. c on the dashboard opens them.

gryt  settings                                       Pivert CLI Server

› Who can join         invite
  Invite means somebody needs a link. Open means anybody who can reach
  the address.
  LAN discovery        on
  Open on the LAN      off
  Profanity filter     off

 ↑/↓ select   space change   esc back

space cycles the selected setting to its next value and sends it straight away. There's no save step. The line under the cursor says what the setting does, which is the part that's hard to guess from the name.

SettingValuesWhat it does
Who can joininvite · openopen lets anybody who can reach the address join without a link
LAN discoveryon · offAdvertises the server over mDNS, so clients on this network list it without being given the address
Open on the LANon · offLets anybody already on this network join without an invite
Profanity filteroff · flag · censor · blockflag marks a message, censor hides the word, block refuses the message

These reach the server through a management API it publishes on loopback, which is also what makes a change take effect rather than only recording it. Turning LAN discovery off has to withdraw the mDNS advertisement, and only the server can do that.

Two consequences. The server has to be running: stopped, the screen says so and suggests s. And the server has to be new enough to carry the API, which means 1.5.0 or later; against anything older the screen says the server has no management API and to update its image and restart it.

Which version a server runs

enter on a server shows the version it's running, under Version. When a newer one exists it shows both:

  Version
  1.4.6 → 1.5.0   restart to pull it

Restarting is what pulls the newer image. The version is read off the container rather than asked of the server, so it works on images built long before the server had any way to report it.

Commands

CommandWhat it does
grytOpen the manager
gryt doctorCheck Docker, the config directory and the ports. Exits non-zero when something is wrong
gryt listPrint each server's ID, address and name
gryt env <server>Print the settings, each marked live or restart. Secrets are masked
gryt channelPrint the release channel this machine follows
gryt channel betaFollow beta, for this CLI and for the images its servers run
gryt updateReplace this binary with the newest release
gryt versionPrint the CLI version

gryt doctor checks that Docker is installed, that the Compose plugin is there, that the daemon is actually running, that the config directory is writable, and that nothing else has taken a server's port. That last one matters on macOS: AirPlay Receiver listens on port 5000, so a server there's reachable by AirTunes rather than by anybody you gave the address to.

ok    Docker installed     /usr/local/bin/docker
ok    Compose plugin       available
FAIL  Docker daemon        installed, but not running
ok    Config directory     /Users/you/Library/Application Support/gryt

Docker daemon: Open Docker Desktop and wait for it to finish starting

Where things live

Profiles go in your user config directory, under gryt/servers/<id>/:

PlatformPath
macOS~/Library/Application Support/gryt
Linux~/.config/gryt
Windows%AppData%\gryt

Set GRYT_CONFIG_DIR to put them somewhere else.

Each server directory holds profile.json, a generated .env, a generated compose.yaml, and a data/ directory mounted into the container. Beside them is shared/, holding the compose project every server uses and the credentials for the object store. Both are created readable only by your user, because they hold secrets.

Generated files carry a header saying they're managed by gryt. Editing them by hand works, but the next save overwrites them.

What gets generated

Two compose projects, on a shared Docker network named gryt.

shared/ holds the pieces there's one of per machine: the SFU that carries voice for every server here, and, when any server uses shared storage, the object store and its bucket. Starting any server brings it up first.

Each server gets its own project: the Gryt server itself, and an image worker beside it when that server uses object storage. The worker is per-server rather than shared because it reads the job queue out of that server's own database.

So voice and uploads work on a server made with the defaults, with nothing to configure and no external service.

Which settings need a restart

Settings marked live are ones that belong in the server's own database rather than its environment, and those are the ones c changes, without a restart. Everything marked restart lives in the generated .env, so it takes effect the next time the server starts.

On this page