# Deployment

## Docker image

Pull the published server image from Docker Hub; no build step is required:

```bash
docker pull xdsoft/jodit-collaboration:latest
docker run -p 8083:8083 xdsoft/jodit-collaboration:latest
```

Image defaults (overridable with `-e`):

```dockerfile
ENV PORT=8083 \
    STATIC_DIR=/app/public \
    ALLOW_ANONYMOUS=true
EXPOSE 8083
CMD ["node", "packages/server/dist/run.js"]
```

The image also bakes a JWT-authenticated production entry file at
`/app/examples/server-prod.mjs`; the Compose file below runs it with a
`command:` override.

> **Warning**
>
> The default `CMD` runs in demo mode (`ALLOW_ANONYMOUS=true`, in-memory
> storage). For production, override the command to
> `node examples/server-prod.mjs` (real JWT auth; see
> [Authentication](authentication.md)) and configure Postgres, as the Compose
> file below does.

## Docker Compose with Postgres

A complete persistent production setup: the published image plus
`postgres:16-alpine` with a data volume, running the JWT-auth entry point.
There is nothing to build or mount; just set a real `JWT_SECRET`:

```yaml
services:
    collab:
        image: xdsoft/jodit-collaboration:latest
        restart: unless-stopped
        command: ['node', 'examples/server-prod.mjs'] # JWT auth entry baked into the image
        environment:
            PORT: 8083
            STORAGE: postgres
            DATABASE_URL: postgres://collab:collab@postgres:5432/collab
            JWT_SECRET: 'change-me-to-a-long-random-secret' # set a real secret
            # ALLOW_ANONYMOUS is intentionally unset → anonymous connections are rejected
        ports: ['8083:8083']
        depends_on: [postgres]
    postgres:
        image: postgres:16-alpine
        environment:
            POSTGRES_USER: collab
            POSTGRES_PASSWORD: collab
            POSTGRES_DB: collab
        volumes: ['collab-db:/var/lib/postgresql/data']
volumes:
    collab-db:
```

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

The Postgres schema is created idempotently by the server on startup; no
migration step is needed (see [Storage](storage.md#postgres)).

## Building from source (maintainers)

Contributors and maintainers can build the image from the repository instead
of pulling it. The multi-stage Dockerfile (`packages/server/Dockerfile`,
`node:22-alpine`) builds both workspace packages, then keeps only production
dependencies, the built `dist` and the static demo page. The build context
must be the repo root, not the package directory:

```bash
docker build -f packages/server/Dockerfile -t jodit-collaboration .
docker run -p 8083:8083 jodit-collaboration
```

## Behind nginx (WebSocket proxying)

The WS endpoint lives under the same prefix as the REST routes (default
`/collab`), so one location block covers everything. The critical parts are
the `Upgrade`/`Connection` headers and a long read timeout (the client
heartbeats every 30 s, so anything comfortably above that works):

```nginx
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 443 ssl;
    server_name example.com;

    # ... ssl_certificate / ssl_certificate_key ...

    location /collab/ {
        proxy_pass http://127.0.0.1:8083;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 120s;
        proxy_send_timeout 120s;
    }

    # optional: the static demo/app served by the collab server itself
    location / {
        proxy_pass http://127.0.0.1:8083;
        proxy_set_header Host $host;
    }
}
```

Point the editor at the TLS endpoint:

```ts
Jodit.make('#editor', {
	collab: { url: 'wss://example.com/collab/ws', docId, token }
});
```

> **Tip**
>
> The server reads the client IP from the socket for guest-identity
> seeding. Behind a proxy all sockets share the proxy's IP, which only
> affects how guest names are seeded; authentication itself is
> token-based and unaffected.

## Health checks

`GET ${ROUTE_PREFIX}/health` (default `/collab/health`) returns:

```json
{ "success": true, "protocol": 1 }
```

It requires no authentication, so it is safe for load-balancer and orchestrator
probes (the compose file and the Docker healthcheck above use it).

## Graceful shutdown

The CLI entry point (`run.js`, used by the Docker image) handles `SIGINT`
and `SIGTERM`: it closes the WS server and all sockets, stops the room
sweeper, closes the HTTP listener and the storage pool, then exits. When
embedding, call `server.stop()` yourself.

Since every sequenced entry is persisted **before** it is broadcast, a
restart never loses acknowledged edits; clients reconnect automatically and
resume via the `sinceSeq` delta (or a fresh snapshot).

## Scaling caveats

The current implementation is a **single-node sequencer**:

- each document (room) lives in exactly one process: the per-document
  sequence numbers, transform window, and presence fan-out are in-memory;
- you can scale **vertically**, and you can shard **by document** (route each
  `docId` to a fixed node, e.g. consistent hashing at the load balancer),
  but two nodes must never serve the same document at the same time;
- as a safety net, the Postgres `(doc_id, seq)` primary key makes a racing
  second writer fail loudly instead of forking the history, but that is a
  guard rail, not a coordination mechanism;
- multi-node fan-out (e.g. Redis pub/sub or Postgres LISTEN/NOTIFY between
  nodes) is **future work**, planned as another adapter.

Practical sizing note: rooms are cheap (a JSON tree plus the recent entry
window) and empty rooms are unloaded after `EMPTY_ROOM_TTL_MS` (default
10 minutes), so a single node comfortably serves many documents; the
binding constraint is concurrent editors per node, not document count.
