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 | shThe 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:
| Variable | Effect |
|---|---|
GRYT_VERSION | Install a specific tag, such as v0.1.0, instead of the newest release |
GRYT_INSTALL_DIR | Install 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@latestStarting a deployment needs Docker Desktop, or Docker Engine with the Compose plugin. Creating profiles and writing their files works without Docker.
Updating
gryt updateThat 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 | shTo 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 backThe 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, oruwhen the dashboard says one is available - Updates your servers:
gryt pull <server>fetches the newer image and recreates the container, andgryt pull --auto ondoes 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
.envfiles are readable only by you
Using the manager
grytEvery 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 hereThe 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.
| Key | Action |
|---|---|
↑ ↓ or k j | Select a server |
enter | One server, with its addresses grouped by who they're for |
n | New server |
e | Edit the selected server |
c | Change the settings the server keeps in its own database |
s x r | Start · stop · restart |
l | Follow the log |
D | Remove the server, once you've typed its id |
g | Refresh now |
u | Update, when one is available |
q | Quit |
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:5001The 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.
| Step | Question | Default |
|---|---|---|
| 1 | Server name | none, required |
| 2 | Bind address | 0.0.0.0, reachable from other machines |
| 3 | Port | the first one nothing else on this machine holds |
| 4 | Security level | balanced |
| 5 | Voice seats | 0, no limit |
| 6 | Trusted proxy hops | 0 |
| 7 | Where will people connect from | this 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 typeTick 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.
| Level | Identities | LAN discovery |
|---|---|---|
strict | Gryt accounts only | Not advertised |
balanced | Gryt accounts only | Advertised |
community | Gryt accounts and local identities | Advertised |
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-servergryt create takes a flag for each question the wizard asks:
| Flag | Same as | Default |
|---|---|---|
--name | Server name | required |
--host | Bind address | 0.0.0.0 |
--port | Port | the first port nothing else on this machine holds |
--security | Security level (strict, balanced or community) | balanced |
--voice-seats | Voice seats | 0, no limit |
--proxy-hops | Trusted proxy hops | 0 |
--domain | The typed address in "Where will people connect from" | none |
--lan | Ticking this machine's own addresses in that same question | off |
--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 backspace 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.
| Setting | Values | What it does |
|---|---|---|
| Who can join | invite · open | open lets anybody who can reach the address join without a link |
| LAN discovery | on · off | Advertises the server over mDNS, so clients on this network list it without being given the address |
| Open on the LAN | on · off | Lets anybody already on this network join without an invite |
| Profanity filter | off · flag · censor · block | flag 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-serverThe 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-serverThat 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 --sharedRecreating 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 settingsIt'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-serverIt 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
| Command | What it does |
|---|---|
gryt | Open the manager |
gryt create --name <name> [flags] --yes | Create 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 doctor | Check Docker, the config directory and the ports. Exits non-zero when something is wrong |
gryt list | Print each server's ID, address and name |
gryt env <server> | Print the settings, each marked live or restart. Secrets are masked |
gryt channel | Print the release channel this machine follows |
gryt channel beta | Follow beta, for this CLI and for the images its servers run |
gryt update | Replace 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 --shared | Pull the voice server every server here shares, and the object store if there is one |
gryt pull --auto on | Check for new images every night, on Linux. Run it again after making a server |
gryt pull --auto off | Remove the nightly timer |
gryt pull --auto status | Say 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 version | Print 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 startingWhere things live
Profiles go in your user config directory, under gryt/servers/<id>/:
| Platform | Path |
|---|---|
| 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.