# Self-host Shappy Backend

Shappy Backend runs ShappyChat on a computer or server you control. It serves the chat app, keeps conversations in celld, and connects your model server, your app tools and the Chrome extension to one place.

> **Status:** This release is for one operator. A multi-user hosted service is not supported. Operator sign-in gives full control of the installation, so run it only for the person who owns the data.

This guide uses the Docker Compose bundle from the release page. The bundle runs two services: `backend` (the chat app and API) and `celld` (the cell host that runs conversations). Both services use the same release version.

## Install

You need:

- Docker with Docker Compose v2. Run `docker compose version` to check.
- A model server on this computer or another reachable host, such as LM Studio or Ollama, or any OpenAI-compatible endpoint.

![The release bundle: two image archives are loaded into Docker; compose.yml starts the backend and celld containers on your server; the backend publishes port 8849 to 127.0.0.1, optionally behind an HTTPS reverse proxy, and reaches your model server through host.docker.internal](/docs/diagrams/self-host.svg)

Download every file of the release into an empty directory. A release has:

| File | Contents |
| --- | --- |
| `compose.yml`, `.env.example` | The stack and its settings |
| `shappychat-backend-<version>-linux-<arch>.tar.gz` | The backend image, for `amd64` or `arm64` |
| `shappychat-celld-<version>-linux-<arch>.tar.gz` | The celld image, for `amd64` or `arm64` |
| `backup.sh`, `restore.sh` | Backup and restore scripts |
| `shappychat-<version>.tgz` | The SDK. The backend also serves it at `/sdk/shappychat.tgz`. |
| `shappychat-extension-<version>.zip` | The Chrome extension |
| `SHA256SUMS`, `provenance.json` | Checksums and build information |

Check the files against the checksums. On macOS, use `shasum -a 256 -c SHA256SUMS`.

```bash
sha256sum -c SHA256SUMS
```

Load the two image archives for this computer. Use `amd64` on x86-64. Use `arm64` on Apple silicon or ARM Linux. Docker does not download these images from a registry.

```bash
docker load -i shappychat-backend-<version>-linux-<arch>.tar.gz
docker load -i shappychat-celld-<version>-linux-<arch>.tar.gz
```

Make the settings file. The `.env.example` of the release sets `SHAPPYCHAT_OWNER` and `SHAPPYCHAT_VERSION` to the names of the images that you loaded. The `compose.yml` of the release uses only these images. It does not build images from the source code.

```bash
cp .env.example .env
```

Start the stack:

```bash
docker compose up -d
docker compose ps
```

Both `backend` and `celld` should show a running state and a healthy status. If one does not, see [Troubleshooting](#troubleshooting).

## First login

Create the operator sign-in link from the backend container:

```bash
docker compose exec backend shappy login
```

The command prints one single-use link. The link is valid for 10 minutes. Open it in a browser on a computer that can reach `SHAPPYCHAT_PUBLIC_URL`. Without a public URL the default is `http://localhost:<port>`, where `<port>` is `SHAPPYCHAT_PORT`, which defaults to 8849.

An operator session lasts 12 hours. A restart of the backend ends every session. After a restart, run `docker compose exec backend shappy login` again.

## Model

Step 2 of the setup flow tests the model server, lists its models and saves the server and the model you choose. `SHAPPYCHAT_MODEL_URL` and `SHAPPYCHAT_MODEL_API_KEY` in `.env` are only the first default values. After you save a server in setup or in **Models**, the backend uses the saved server and model. This is also true after a restart.

A container cannot reach `127.0.0.1` on your computer, because that address is the container itself. For a model server on the same computer, use `host.docker.internal`:

- LM Studio: `http://host.docker.internal:1234/v1`
- Ollama: `http://host.docker.internal:11434/v1`

```dotenv
SHAPPYCHAT_MODEL_URL=http://host.docker.internal:1234/v1
SHAPPYCHAT_MODEL_API_KEY=
```

`SHAPPYCHAT_MODEL_API_KEY` is secret. Leave it empty for local servers that need no key. An empty value does not remove a key saved in the app. The backend encrypts the keys that you save in the app with `master.key`.

## MCP

To add an app that has an HTTP MCP endpoint, open **Tools / MCP** in the backend pages. Then use step 3 of the setup flow to turn on the connections that you added. Enter the MCP URL and the scoped bearer key that the app issued for ShappyChat. Use a key that grants only the operations the chat should use. The app still checks every tool call against its own permissions.

Stdio MCP apps run a command inside the backend container. The backend image contains only Node.js 22 and the backend itself. A stdio app works only when its command and every dependency are inside the image. The bundle does not include other runtimes, so prefer an HTTP MCP endpoint for apps that need them.

You configure a stdio connection in a JSON file. The backend reads this file through `SHAPPYCHAT_CONFIG`. Mount the file with a compose override. Save this text as `compose.override.yml` next to `compose.yml`. Docker Compose loads it automatically:

```yaml
services:
  backend:
    environment:
      SHAPPYCHAT_CONFIG: /config/shappychat.json
    volumes:
      - ./shappychat.json:/config/shappychat.json:ro
```

```json
{
  "connections": [
    {
      "transport": "stdio",
      "id": "notes",
      "command": "node",
      "args": ["/opt/notes-mcp/index.js"],
      "inheritEnv": ["NOTES_API_TOKEN"]
    }
  ]
}
```

The command path must exist inside the backend container. `inheritEnv` gives the names of the variables that the command gets from the backend environment. Put their values in `.env`. Then run `docker compose up -d` to make the backend again. A command that is not in the image needs a custom image. Build the custom image from `backend.Dockerfile`. The release files do not include `backend.Dockerfile`.

## App credential

Step 4 of the setup flow issues a credential for each app or client that uses the chat. For each one, enter:

- The exact origins that may use the credential, for example `https://app.example.com`.
- For the Chrome extension, its origin `chrome-extension://<ID>`, where `<ID>` is the extension ID in your browser.
- The MCP connections that the client may use.

The backend shows the credential one time only. Copy it when the backend shows it. ShappyChat keeps only a hash of the credential, so it cannot show it again. If you lose the credential, issue a new one and revoke the old one.

Keep the credential on the app's server. Do not place it in browser code, public repositories or client bundles. Revoke it under **Connected apps** to deny access and cancel the turns that it started.

Step 6 of the setup flow gives the agent instructions and the code for the app. The backend supplies the SDK at `<public URL>/sdk/shappychat.tgz`, for example `https://chat.example.com/sdk/shappychat.tgz`.

## HTTPS reverse proxy

Users on other computers need an HTTPS address. Put a reverse proxy in front of the backend and keep the backend on loopback. In `.env`:

```dotenv
SHAPPYCHAT_PUBLIC_URL=https://chat.example.com
SHAPPYCHAT_BIND=127.0.0.1
SHAPPYCHAT_PORT=8849
```

Run `docker compose up -d` to apply the change. `SHAPPYCHAT_BIND=127.0.0.1` publishes the backend port only to this computer, so the proxy is the only route in. The proxy must forward to `127.0.0.1:8849`, or to the value of `SHAPPYCHAT_PORT`.

Caddy obtains and renews the certificate for the host name. Caddy forwards the original Host header by default:

```caddyfile
chat.example.com {
	reverse_proxy 127.0.0.1:8849
}
```

With nginx, provide the certificate yourself. Forward the original Host header with `$http_host`, which keeps any port. Turn off response buffering and allow long reads, because the app streams events to the browser:

```nginx
server {
    listen 443 ssl;
    server_name chat.example.com;

    ssl_certificate     /etc/ssl/certs/chat.example.com.pem;
    ssl_certificate_key /etc/ssl/private/chat.example.com.key;

    location / {
        proxy_pass http://127.0.0.1:8849;
        proxy_set_header Host $http_host;
        proxy_http_version 1.1;
        proxy_buffering off;
        proxy_read_timeout 1h;
        proxy_send_timeout 1h;
    }
}
```

The backend accepts a request only when its Host header matches an allowed host. The allowed hosts are the public URL host, including its port when it is not the default, and the local addresses on this computer. A proxy that rewrites the Host header causes a 403 response. When `SHAPPYCHAT_PUBLIC_URL` uses `https`, the backend marks the operator cookie `Secure`. Thus you must sign in through the HTTPS address.

## Upgrade

Each release has its own image tag. Stop the stack, take a backup, then move to the new version:

```bash
docker compose stop
```

Make a backup as [Backup](#backup) tells you. Then do these steps:

1. Download the files of the new release.
2. Check the files with `SHA256SUMS`.
3. Load the two image archives of the new release with `docker load -i`.
4. Compare the new `.env.example` with your `.env`. Add the new variables to `.env`.
5. Set the new version:

```dotenv
SHAPPYCHAT_VERSION=<new-release-version>
```

```bash
docker compose up -d
docker compose ps
```

The data directory records its format in `data-version.json`. This release writes format 1. If a backend starts with data written by a newer build, it refuses to start and reports:

```text
This data was written by a newer Shappy Backend (format N). Upgrade Shappy Backend or restore a backup.
```

Replace `N` with the format number in the message. To return to an older version, restore a backup taken before the upgrade. Do not run an older image against data from a newer one.

## Backup

The release download includes `backup.sh` and `restore.sh`. Run them on the machine that runs the bundle, from the directory that holds them, and keep the archive private. Both scripts run the small `alpine:3` image to read and write the volumes, so an offline host must load that image first.

Stop the project before a backup, so the copy is consistent. The compose project name is `shappychat` by default, from the `name:` entry in `compose.yml`.

```bash
docker compose -p shappychat stop
./backup.sh shappychat ~/backups/shappychat-2026-10-09.tar.gz
docker compose -p shappychat up -d
```

`./backup.sh <compose-project> <output.tar.gz>` archives the `data` and `celld-state` volumes into one file with mode 600. It refuses to overwrite an existing file, so choose a new name for each backup. The archive contains `master.key`, every stored provider and MCP secret, and all conversations. Store it outside the repository, with owner-only permissions, and do not share it.

Restore into a stopped project:

```bash
docker compose -p shappychat stop
./restore.sh shappychat ~/backups/shappychat-2026-10-09.tar.gz
docker compose -p shappychat up -d
```

`./restore.sh [--force] <compose-project> <archive.tar.gz>` creates any missing volumes and restores the archive into them. It refuses to run while the project has running containers. It refuses to write into volumes that already hold data, unless you pass `--force`, which replaces their content with the archive. Use a different project name to restore into new volumes without touching the current ones.

The archive includes `master.key`, so a restore brings back the key that encrypts the stored secrets. If you set `SHAPPYCHAT_MASTER_KEY`, the backend uses this value and not `master.key`. This value must be the key that encrypted the data.

## Troubleshooting

**(a) The browser shows "Unexpected runtime host" with status 403.** The Host header does not match an allowed host. Set `SHAPPYCHAT_PUBLIC_URL` to the address in the browser, including the scheme and the port, for example `http://localhost:8849`. Run `docker compose up -d` after the change. If a proxy is in front, check that it forwards the original Host header.

**(b) The public address check reports "Local only".** The public URL is `localhost`, `127.0.0.1` or `[::1]`. That works on this computer only. For other computers, set `SHAPPYCHAT_PUBLIC_URL` to an `https` address and put a reverse proxy in front, as described in [HTTPS reverse proxy](#https-reverse-proxy). A public URL that uses `http` on another host is reported with "Use an https public URL behind a reverse proxy".

**(c) The celld check fails.** The message reads "celld did not respond at <url>". Check the service and its log:

```bash
docker compose ps
docker compose logs celld
```

If the log shows "Waited 120 seconds for the backend tokens", the backend did not write its service tokens. Read `docker compose logs backend`, fix the backend start, then restart celld with `docker compose restart celld`.

**(d) The model test fails from Docker.** A container cannot use `localhost` or `127.0.0.1` for a model on your computer. Set `SHAPPYCHAT_MODEL_URL` to `http://host.docker.internal:1234/v1` for LM Studio or `http://host.docker.internal:11434/v1` for Ollama. The model server must also accept connections from outside itself. Check its listen address if the test still fails. The tests of the bundle do not include this check.

**(e) The backend reports "Cannot decrypt ... wrong master key or damaged file".** A different master key encrypted the file. Restore the `master.key` that matches the data, from the backup or the original installation, into the data volume. Alternatively set `SHAPPYCHAT_MASTER_KEY` to the base64 key that encrypted the data. Do not start a new installation over the data. Without the key, you cannot recover the stored secrets.

**(f) The sign-in link has expired.** A link is valid for 10 minutes. You can use it one time only. Run `docker compose exec backend shappy login` again.

**(g) Where the backend keeps secrets.** The backend encrypts the provider keys and MCP keys in the app settings with AES-256-GCM and `master.key` in the data volume. To supply the key from outside the volume, set `SHAPPYCHAT_MASTER_KEY`. The celld service tokens are in plain files: `celld.json` in the data volume and `celld.env` in the celld config volume. The backend writes both files with mode 0600. The backend keeps only hashes of app credentials. Keep backups and `.env` private, because `.env` contains the model key.
