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

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.

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.

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.

cp .env.example .env

Start the stack:

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.

First login

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

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

services:
  backend:
    environment:
      SHAPPYCHAT_CONFIG: /config/shappychat.json
    volumes:
      - ./shappychat.json:/config/shappychat.json:ro
{
  "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:

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:

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:

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:

docker compose stop

Make a backup as 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:
SHAPPYCHAT_VERSION=<new-release-version>
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:

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.

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:

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. 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 ". Check the service and its log:

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.

↑ ↓ select · Enter open · Escape close