# Deploying Meilisearch This document covers production deployment of the Meilisearch service that backs `packages/search` and the `search-indexer` worker job. Local development uses the bundled docker-compose service — see `docker-compose.yml` and `docs/features/search.md` for the application-side contract. ## Requirements - Disk: 1–10 GiB per million indexed documents, SSD strongly recommended. - Memory: 1 GiB minimum; allocate ~30 % of the dataset size for hot reads. - Network: reachable only from `apps/web`, `apps/api` and the `worker-general` service. **Never** expose port 7700 to the public internet. The master key is the only auth boundary; if the port is open, the index is open. - TLS: terminate at the platform's ingress / load balancer; Meilisearch itself does not speak TLS. ## Environment Variables | Variable | Required | Purpose | | --- | --- | --- | | `MEILI_URL` | yes | URL the search wrapper and worker dial. Internal DNS only (e.g. `http://meilisearch.internal:7700`). | | `MEILI_MASTER_KEY` | yes | Server-side secret. Min 32 bytes of entropy. Rotate via blue/green; never log. | | `MEILI_ENV` | yes | `production` in production. Disables the unauthenticated dashboard. | | `MEILI_NO_ANALYTICS` | recommended | Set to `true` to disable upstream telemetry. | ## Coolify (Hetzner) — recommended for the staging stack Coolify ships a "custom docker-compose" service type that maps 1:1 onto the local layout. Paste the snippet below into the project's docker-compose override (Coolify → Service → Configuration → Compose). ```yaml services: meilisearch: image: getmeili/meilisearch:v1.8 environment: - MEILI_MASTER_KEY=${MEILI_MASTER_KEY} - MEILI_ENV=production - MEILI_NO_ANALYTICS=true volumes: - meilisearch-data:/meili_data healthcheck: test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:7700/health | grep -q 'available'"] interval: 10s timeout: 5s retries: 6 start_period: 15s restart: unless-stopped networks: - backend volumes: meilisearch-data: networks: backend: external: true ``` Operator steps: 1. Set `MEILI_MASTER_KEY` as a Coolify "secret" (Project → Variables → "Encrypt"). Length ≥ 32 bytes; generate with `openssl rand -hex 32`. 2. Set `MEILI_URL=http://meilisearch:7700` on `apps/web`, `apps/api` and the `worker-general` service so they resolve via the internal Docker network. Do **not** map the port to the host. 3. Add a backup cron that snapshots the `meilisearch-data` volume: ```bash # /etc/cron.daily/meilisearch-snapshot docker exec meilisearch \ curl -X POST -H "Authorization: Bearer $MEILI_MASTER_KEY" \ http://127.0.0.1:7700/snapshots ``` 4. Verify `/health` is `available` after deploy; the worker logs `meilisearch.connected` on cold start. ## Dedizierte VM — alternative (Hetzner Cloud) For traffic spikes that outgrow the central Coolify host, run Meilisearch on a dedicated Hetzner Cloud VM (e.g. CX22) with a 10 GB volume: ```bash # Hetzner: attach a 10 GB volume and mount at /meili_data ssh root@ docker run -d --name meilisearch \ --restart unless-stopped \ -p 7700:7700 \ -v /meili_data:/meili_data \ -e MEILI_MASTER_KEY="$(cat /etc/meilisearch/master-key)" \ -e MEILI_ENV=production \ -e MEILI_NO_ANALYTICS=true \ getmeili/meilisearch:v1.8 ``` Front the instance with a private network; never expose port 7700 to the public internet. Apps reach it via the private DNS name configured in the `MEILI_URL` env. ## Master-Key Rotation 1. Generate a new key: `openssl rand -hex 32`. 2. Set the new key as `MEILI_MASTER_KEY_NEW` in the secret store. 3. Bring up a parallel Meilisearch container with the new key; replicate indexes via the snapshot/restore endpoint: ```bash curl -X POST -H "Authorization: Bearer $OLD_KEY" \ "$OLD_MEILI/snapshots" # copy the snapshot file, start new container pointing at it ``` 4. Flip `MEILI_URL` on the apps to point at the new container. 5. Decommission the old container; rotate the secret-store key (move `MEILI_MASTER_KEY_NEW` to `MEILI_MASTER_KEY`). The wrapper's transparent SQL-LIKE fallback (`searchWithFallback`) keeps search degraded-but-up during the cutover. ## Disaster Recovery The Postgres database remains the source of truth. To rebuild Meilisearch from scratch: ```bash pnpm cli search reindex --tenant=all --index=all ``` This writes `search.index..bulkreindex` outbox events only for the currently supported bulk-reindex set. Today `--index=all` expands to KB articles, CRM contacts, rows, and users. Entities are rejected until their tenant-scoped projection has an explicit pager design. The `search-indexer` job consumes the accepted events and pages over Postgres in 1000-row chunks. Expected throughput: ~10 k documents per minute per worker replica. ## Backup Retention Snapshots are full copies of the data directory. Retain at least 7 daily snapshots offsite (S3 / Backblaze). The volume itself is also covered by the platform's nightly volume backup if you keep that enabled.