# Install the server

> Run zidane-server from Docker, a release binary, npm, the zidane command or a checkout, then create the first token.

Every way below runs the same server.

| Way | Good for | Command |
| - | - | - |
| Docker image | Servers, Compose, Coolify | `docker run ghcr.io/tahul/zidane-server:8.1.0` |
| Release binary | A VPS with systemd, no container | `zidane-server start` |
| npm | A machine with Node, no install step | `npx zidane-server start` |
| The `zidane` command | A machine that already has the terminal app | `zidane server start` |
| From source | Working on zidane | `bun run server start` |

## Run it

**Docker**

```bash
docker run -d --name zidane --restart unless-stopped \
  -v zidane-data:/data -p 127.0.0.1:4280:4280 \
  --memory 4g --pids-limit 2048 --security-opt no-new-privileges \
  ghcr.io/tahul/zidane-server:8.1.0
```

The image is public: no `docker login`. Images exist for `linux/amd64` and `linux/arm64`. The agents run commands in the container: `--memory` and `--pids-limit` cap what they take from the host (raise them for heavy builds), and `no-new-privileges` keeps a setuid binary from gaining anything.

| Tag | Tracks |
| - | - |
| `8.1.0` | That release. It never moves (use it) |
| `8.1` | Its patch releases |
| `8` | Its minor and patch releases |
| `latest` | The newest release |

The release workflow publishes a pre-release under its own version only (`8.2.0-beta.1`), never under `8.2`, `8` or `latest`.

The image sets `ZIDANE_SERVER_BEHIND_PROXY=1`, so a direct WebSocket connection to `127.0.0.1:4280` gets a 403. Put a TLS proxy in front ([Compose with Caddy](https://zidane.si/docs/server-deploy.md#docker-compose)). Token commands and health checks run through `docker exec` and the unix socket, so they need no proxy.

**Release binary**

```bash
curl -fsSL https://zidane.si/install | sudo sh -s -- --server --prefix /usr/local
zidane-server start            # 127.0.0.1:4280
```

The install script downloads the archive for your machine from `https://zidane.si/dl`, checks its SHA-256 against the release manifest (and the manifest's signature when minisign is installed), and installs `zidane-server` to `/usr/local/bin` with its web app in `/usr/local/share/zidane-server`. Without `sudo` and `--prefix`, it installs to `~/.local/bin`. `--version <v>` installs the release before the latest.

By hand, the archives are at `https://zidane.si/dl/v<version>/`, and the [download page](https://zidane.si/download) lists them with their checksums:

```bash
V=8.1.0 T=linux-x64            # darwin-arm64, darwin-x64, linux-x64, linux-arm64
curl -fLO "https://zidane.si/dl/v$V/zidane-server-v$V-$T.tar.gz"
tar -xzf "zidane-server-v$V-$T.tar.gz"
sudo install -m 0755 "zidane-server-v$V-$T/zidane-server" /usr/local/bin/
```

Check the archive first: [verify a download](https://zidane.si/docs/verify.md). The server keeps the latest release and the one before it.

The binary is one self-contained executable. The linux archives also hold `zidane-server.service`, a systemd unit ([VPS with systemd](https://zidane.si/docs/server-deploy.md#vps-with-systemd)). Linux builds need glibc (Debian, Ubuntu, Fedora). They do not run on Alpine (musl). macOS builds are signed with an Apple Developer ID and notarized.

**npm**

```bash
npx zidane-server start        # or: npm install -g zidane-server
```

The `zidane-server` package is a small launcher. On first run it downloads the binary for your machine from `https://zidane.si/dl`, checks the release signature and the SHA-256, and keeps it in a cache for the next runs.

**zidane command**

```bash
npm install -g zidane-tui      # or: bun add -g zidane-tui
zidane server start
```

The [`zidane` command](https://zidane.si/docs/tui.md#install-and-run) includes the server. `zidane server <command>` is the same as `zidane-server <command>`.

**From source**

```bash
bun install
bun run server start                     # the server, from src/server/main.ts
bun run server:build                     # or the binary: server/dist/zidane-server
bun run server:docker                    # or the image: zidane-server:dev
```

`bun run server:smoke` and `bun run server:smoke:image` test the binary and the image. `bun run server:release:package` writes the release archives and `SHA256SUMS` to `server/dist/release/`. In a checkout, `server/bin/zidane-server.mjs` runs the built binary, or the terminal app's `zidane server` when there is none.

## Versions

Pin an exact release (`8.1.0`), and upgrade the server and its clients together. A client and the server must share a protocol version. Each side speaks a range of them, so a client one release away usually still attaches, but a release can move the range past older clients (a patch release once changed the version, `v7.0.10`). On a moving tag, a plain redeploy can leave every client refused until it is upgraded too. The [upgrade steps](https://zidane.si/docs/server-deploy.md#upgrades) back up and wait for running work first.

## What it prints at start

One line on stderr, then a hint while no token exists:

```text
✓ zidane server listening on ws://127.0.0.1:4280 and /home/me/.config/zidane/server.sock (data /home/me/.config/zidane, pid 4242)
Next: create a client token: zidane server token create --name <device> (there is none yet. This works while the server runs.)
```

Without `--data-dir`, the server uses zidane's usual folders ([where they are](https://zidane.si/docs/files.md#base-folders)). A deployment passes `--data-dir <dir>` to keep everything under `<dir>/.zidane`.

The server listens on loopback by default and refuses any other interface without TLS or a proxy in front ([Deploy](https://zidane.si/docs/server-deploy.md)).

## Create the first token

Create one token per device. The server prints it once on stdout and keeps only its hash.

```bash
zidane-server token create --name laptop                    # binary
docker exec zidane zidane-server token create --name laptop # image
```

Keep it in a file only you can read (`chmod 600`). If you have no shell before the first deploy, use a [bootstrap token](https://zidane.si/docs/server-security.md#bootstrap-token). For scopes, rotation and revoking, see [Tokens](https://zidane.si/docs/server-security.md#tokens).

To run session commands on other computers, add each one as a machine. A join code, a one-time code from the server, adds it ([servers and machines](https://zidane.si/docs/servers-and-machines.md)).

## Check it runs

On the server's machine:

```bash
zidane-server health          # over the unix socket
zidane-server health --http   # GET /healthz on the server's port
```

Both exit 0 when the server is healthy ([output](https://zidane.si/docs/server-operations.md#health)).

## Connect a window

Clients send a token over `ws://` only to `127.0.0.1` and `localhost`. To test from another machine before TLS is set up, open an SSH tunnel:

```bash
ssh -L 4280:127.0.0.1:4280 box
zidane --host ws://127.0.0.1:4280 --token-file box.token
```

To save the server in an app or connect a browser, see [Clients](https://zidane.si/docs/server-clients.md).

## Web app

The server can serve zidane's web app at `/` on its own port: a chat in the browser, with no other host to run. It is off by default. Turn it on with `--web` (or `ZIDANE_SERVER_WEB=1`, or `"web": true` in `server.json`), then log a browser in with a pairing code:

```bash
zidane-server start --web
zidane-server pair             # prints a code like 7KQM-3XFD, valid 10 minutes
```

Open the server's address (`https://box.example.com/`, or `http://127.0.0.1:4280/` through the SSH tunnel above) and type the code. The browser gets its own device token, `browser: <name>` in `zidane-server token list`. A `chat` code (the default) lets it chat and answer tool approvals, `--scope admin` gives it everything ([pairing rules](https://zidane.si/docs/server-security.md#pairing)).

Where the app comes from:

| Way | App |
| - | - |
| Docker image | Bundled, in `/usr/local/share/zidane-server/web-dist` |
| Release binary | `web-dist/` in the archive. `zidane server setup` installs it next to the binary, and auto-update replaces both together. By hand, keep it next to the binary, or install it as `/usr/local/share/zidane-server/web-dist` beside `/usr/local/bin/zidane-server`: `sudo mkdir -p /usr/local/share/zidane-server && sudo cp -R "zidane-server-v$V-$T/web-dist" /usr/local/share/zidane-server/` |
| The `zidane` command | Not bundled: pass `--web-root <dir>` |
| From source | `bun run server:web` builds `web/` into `server/web-dist/` |

`--web-root <dir>` (`ZIDANE_SERVER_WEB_ROOT`, `webRoot` in `server.json`) serves another build: a directory with an `index.html`. With `--web` on and no app found, the server refuses to start and says why. `--no-web` turns it off over the variable and the file.

The app connects back over the same origin (`wss://<host>/`), so it needs no entry in `ZIDANE_SERVER_ALLOWED_ORIGINS`. Every page carries a strict Content-Security-Policy (`default-src 'self'`, `frame-ancestors 'none'`), `X-Content-Type-Options: nosniff` and `Referrer-Policy: no-referrer`. Hashed files under `/assets/` are cached for a year, `index.html` never.
