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 media worker.

Servers already running keep the image they have. gryt pull <server> moves one of them onto the channel you're on now, and says so when it does:

Pivert CLI Server runs ghcr.io/gryt-chat/server:latest, and this machine
follows beta now. Pulling the latest-beta images.

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 per machine, shared by every server on it, and uploads in each server's own folder
  • 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
  • Updates your servers: gryt pull <server> fetches the newer image and recreates the container, and gryt pull --auto on does it every night on Linux
  • Cleans up after itself: gryt remove <server> deletes a server and everything it made
  • 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   —        folder
› Pivert CLI Server            ● running  192.168.1.42:5001   ready    folder
  Staging                      ○ stopped  192.168.1.42:5002   —        folder

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

The row under SHARED is the SFU, which this machine runs one of. l follows its output, which is where to look when voice stops working. x stops it for every server at once, and says so.

A machine with servers made by an older version of gryt has a second row, Object store. That's the MinIO those servers keep their uploads in, and it stays for as long as one of them uses it.

A media 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
DRemove the server, once you've typed its id
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.

Seven questions, and ticking "a domain" adds an eighth.

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

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.

Uploads

There's no question about them. Each server keeps its uploads in its own data/gryt folder, with a media worker beside it for thumbnails and compression. A server made by an older version keeps the storage it was set up with, and editing it in the wizard doesn't change that.

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.

Creating and starting a server from a script

The wizard and the manager are a terminal-only thing, so scripting a server's setup meant driving them through something like tmux. gryt create and gryt start take flags instead:

gryt create --name my-server --port 5000 --yes
gryt start my-server

gryt create takes a flag for each question the wizard asks:

FlagSame asDefault
--nameServer namerequired
--hostBind address0.0.0.0
--portPortthe first port nothing else on this machine holds
--securitySecurity level (strict, balanced or community)balanced
--voice-seatsVoice seats0, no limit
--proxy-hopsTrusted proxy hops0
--domainThe typed address in "Where will people connect from"none
--lanTicking this machine's own addresses in that same questionoff

--domain takes a ws:// or wss:// address, and can be repeated or given as a comma-separated list. Without --domain or --lan, the server is only reachable at ws://localhost:5005 — the same as leaving every box but "This machine only" unticked.

Without --yes, gryt create only previews what it would write:

My Server (my-server)
  listening on 0.0.0.0:5000
  security: balanced, voice seats: unlimited, proxy hops: 0
  reachable at: ws://localhost:5005

Nothing created. Re-run with --yes to write it.

It refuses a name that's already in use, either way, rather than overwriting that server's secrets.

gryt start <server> brings up the shared voice server, then the named one — the same two steps behind the manager's s key. Both commands exit non-zero on any failure, so a script can chain them with the existing gryt pull and gryt remove without checking anything but the exit code.

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   gryt pull pivert-cli-server

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.

Restarting doesn't pull anything. docker compose restart reruns the container you already have, which is why the line names a command instead:

gryt pull pivert-cli-server

That pulls the image the channel points at, recreates the server and its image worker, and prints the version it was on next to the version it's on now:

Pivert CLI Server runs 1.10.14. The newest release is 1.10.21.

Pulling the latest images for the server and its media worker.
 Image ghcr.io/gryt-chat/server:latest Pulled

Recreating Pivert CLI Server.

Pivert CLI Server: 1.10.14 → 1.10.21.

It stops before doing anything when the server isn't running, or when the server already runs the newest release. --force pulls without asking GitHub what's newest, which is the way through when you can't reach it.

It fetches every image before it replaces any container. A pull that dies halfway leaves the server up on the image it already had, and says so. Running it again is safe. The images that did arrive are still there, so the second run is quicker.

The voice server

The SFU is a compose project of its own, shared by every server on the machine. So is the object store, on a machine that still runs one. gryt pull <server> leaves them alone, and says it did. Move them yourself:

gryt pull --shared

Recreating them interrupts voice and uploads for every server on the machine. That's why it's a separate command, rather than something one server's update does on the way past.

Updating every night

On Linux, gryt pull --auto on installs a systemd timer that checks for newer images once a night, so you don't have to remember to run gryt pull:

gryt pull --auto on       # install the timer, or refresh its list of servers
gryt pull --auto status   # says whether it's on
gryt pull --auto off      # remove the timer and its settings

It's the same timer the Docker Compose guide installs, pointed at the containers this machine's servers run in: each server, its media worker, and the shared voice server. It asks for sudo, because the timer and its settings live under /etc.

Some time between 03:00 and 06:00 it pulls whatever tag your channel names, and recreates only the containers whose image changed. The voice server waits until nobody's in a call, so nobody gets cut off halfway through one.

The list of containers is written when you run on. Make a new server and you'll need to run gryt pull --auto on again, or the timer won't know about it. The log is at journalctl -u gryt-auto-update -n 50.

If you turned it on with gryt 0.7.1 or older, run on again. Those versions left the timer recreating servers without their management token, so c stopped working after the timer's first run, and the voice server was never updated at all.

It needs systemd, so it's Linux only. On macOS and Windows, run gryt pull yourself.

Removing a server

gryt remove pivert-cli-server

It says what's going, then asks you to type the server's id:

This deletes Pivert CLI Server for good:
  its containers, gryt-pivert-cli-server and gryt-pivert-cli-server-image-worker
  /home/you/.config/gryt/servers/pivert-cli-server, which holds its database, its uploads and its settings

Type pivert-cli-server to delete it:

There's no undo. The database and every upload go with it, so copy data/ somewhere first if you might want it back.

When it's the last server on the machine, the shared services go too: the voice server, the gryt network and the shared/ folder, plus the object store and its uploads on a machine that still runs one. The images stay, in case you make another server. It prints the docker image rm line that frees the space.

--yes skips the question, for scripts. D in the manager does the same job and asks the same way.

On Linux the server's files belong to uid 1001, the user it runs as inside its container, so you can't delete them yourself without sudo. gryt remove deletes them through a container instead.

If the nightly update timer is on, it tells you what to run to take the server off the timer's list.

Commands

CommandWhat it does
grytOpen the manager
gryt create --name <name> [flags] --yesCreate a server without the wizard. Previews what it would write until --yes is added
gryt start <server>Start a server created with gryt create or the wizard
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 pull <server>Pull the newest images for one server and recreate it. --force skips the check for a newer release
gryt pull --sharedPull the voice server every server here shares, and the object store if there is one
gryt pull --auto onCheck for new images every night, on Linux. Run it again after making a server
gryt pull --auto offRemove the nightly timer
gryt pull --auto statusSay whether the nightly timer is on
gryt remove <server>Delete a server, its containers and its data, after you type its id. --yes skips the question
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 on a machine that still runs one. Both are created readable only by your user, because they hold secrets.

admin.env sits beside .env and holds the token the manager uses for c. It's a file of its own so .env stays safe to paste into a bug report.

On Linux, data/ belongs to uid 1001, which is the user the server runs as inside its container. A short-lived data-init container hands the folder over each time the server starts, so reading it from your own account needs sudo. A server made on Linux by gryt 0.7.1 or older couldn't write this folder at all. Stop it with x and start it with s, and it picks up the fix.

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 the object store while a server made by an older version still uses it. Starting any server brings it up first.

Each server gets its own project: the Gryt server itself, and an image worker beside it. Uploads go in the server's data/gryt folder, which both of them mount. 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