# The gryt CLI

Source: https://docs.gryt.chat/docs/host/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

```bash
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:

| 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](https://github.com/Gryt-chat/cli/releases) instead.

From source, with Go 1.25 or newer:

```bash
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

```bash
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:

```bash
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:

```bash
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:

```bash
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

```bash
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.

| 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 |
| `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: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.

| 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 |
| 8 | Where do uploads go | `shared` |

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

| Answer | What happens |
|--------|--------------|
| `shared` | The object store on this machine, with thumbnails and compression. Needs nothing from you |
| `filesystem` | Straight into the server's own folder. No extra containers, no thumbnails |
| `s3` | A 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.

| 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](https://docs.gryt.chat/docs/use/accounts)
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.

| 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   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

| Command | What it does |
|---------|--------------|
| `gryt` | Open the manager |
| `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 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 starting
```

## Where 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. 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.

<Callout title="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.
</Callout>
