5.0 KiB
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/apiand theworker-generalservice. 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).
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:
-
Set
MEILI_MASTER_KEYas a Coolify "secret" (Project → Variables → "Encrypt"). Length ≥ 32 bytes; generate withopenssl rand -hex 32. -
Set
MEILI_URL=http://meilisearch:7700onapps/web,apps/apiand theworker-generalservice so they resolve via the internal Docker network. Do not map the port to the host. -
Add a backup cron that snapshots the
meilisearch-datavolume:# /etc/cron.daily/meilisearch-snapshot docker exec meilisearch \ curl -X POST -H "Authorization: Bearer $MEILI_MASTER_KEY" \ http://127.0.0.1:7700/snapshots -
Verify
/healthisavailableafter deploy; the worker logsmeilisearch.connectedon 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:
# Hetzner: attach a 10 GB volume and mount at /meili_data
ssh root@<instance> 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
- Generate a new key:
openssl rand -hex 32. - Set the new key as
MEILI_MASTER_KEY_NEWin the secret store. - Bring up a parallel Meilisearch container with the new key; replicate
indexes via the snapshot/restore endpoint:
curl -X POST -H "Authorization: Bearer $OLD_KEY" \ "$OLD_MEILI/snapshots" # copy the snapshot file, start new container pointing at it - Flip
MEILI_URLon the apps to point at the new container. - Decommission the old container; rotate the secret-store key (move
MEILI_MASTER_KEY_NEWtoMEILI_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:
pnpm cli search reindex --tenant=all --index=all
This writes search.index.<entity>.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.