Skip to Content

Production Guide: Deploy Meilisearch with Docker Compose + NGINX + UFW on Ubuntu

A production-oriented Meilisearch deployment with reverse proxy, API key hygiene, backups, and operations runbook for small teams.

Search quality often decides whether users complete a workflow or abandon it. For product catalogs, documentation hubs, and internal knowledge bases, Meilisearch offers fast, typo-tolerant results with a straightforward API. This guide shows how to deploy Meilisearch in production on a single Ubuntu host using Docker Compose, front it with NGINX, and harden network exposure using UFW. The runbook is designed for teams that need reliable day-2 operations: predictable upgrades, backup/restore confidence, safe key rotation, and practical failure recovery.

The scenario: you run a customer-facing web app and need low-latency full-text search without operating a heavyweight cluster. You want HTTPS termination, private backend networking, and clean operational boundaries between app engineers and platform owners. By the end, you will have a hardened deployment pattern you can reuse across staging and production.

Architecture and flow overview

This deployment uses three layers:

  • Meilisearch container on an internal Docker network, not publicly exposed.
  • NGINX on the host as the only public entrypoint, proxying API traffic to Meilisearch.
  • UFW firewall to allow only SSH/HTTP/HTTPS and deny everything else by default.

Request flow: client application → NGINX (TLS + request size/rate guardrails) → internal Meilisearch endpoint. Administrative operations (snapshot, dump, index settings) occur over authenticated API calls and can be executed from trusted runners or bastion hosts.

Data durability is handled through mounted volumes and scheduled snapshots. Configuration is kept in environment files with strict ownership, while API keys are rotated using Meilisearch key management endpoints. This separation keeps credentials out of git and makes auditing easier.

Prerequisites

  • Ubuntu 22.04/24.04 host with sudo privileges.
  • A DNS record pointed to your server (for example search.example.com).
  • Docker Engine + Compose plugin installed.
  • NGINX installed on host (or in a dedicated reverse-proxy VM).
  • Basic familiarity with curl and JSON payloads.

Capacity starting point: 2 vCPU, 4–8 GB RAM, SSD-backed volume. If your workload includes large faceted indexes or frequent reindexing, plan headroom (8–16 GB RAM) and monitor memory pressure early.

Step-by-step deployment

1) Prepare directory layout and permissions

sudo mkdir -p /opt/meilisearch/{data,snapshots,config}
sudo adduser --system --group --home /opt/meilisearch meiliops || true
sudo chown -R meiliops:meiliops /opt/meilisearch
sudo chmod 750 /opt/meilisearch /opt/meilisearch/*

If copy button does not work in your browser/editor, select the block and copy manually.

2) Create environment file

cat >/opt/meilisearch/config/.env <<'EOF'
MEILI_ENV=production
MEILI_NO_ANALYTICS=true
MEILI_HTTP_ADDR=0.0.0.0:7700
MEILI_MASTER_KEY=replace_with_64_char_random_secret
MEILI_DB_PATH=/meili_data
MEILI_SNAPSHOT_DIR=/meili_snapshots
MEILI_LOG_LEVEL=INFO
EOF
chmod 640 /opt/meilisearch/config/.env
chown meiliops:meiliops /opt/meilisearch/config/.env

If copy button does not work in your browser/editor, select the block and copy manually.

3) Define Docker Compose stack

services:
  meilisearch:
    image: getmeili/meilisearch:v1.12
    container_name: meilisearch
    restart: unless-stopped
    env_file:
      - /opt/meilisearch/config/.env
    volumes:
      - /opt/meilisearch/data:/meili_data
      - /opt/meilisearch/snapshots:/meili_snapshots
    networks:
      - meili_internal
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:7700/health"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 20s

networks:
  meili_internal:
    driver: bridge

If copy button does not work in your browser/editor, select the block and copy manually.

4) Launch service

cd /opt/meilisearch
docker compose -f docker-compose.yml up -d
docker compose ps
docker compose logs --tail=100 meilisearch

If copy button does not work in your browser/editor, select the block and copy manually.

5) Configure NGINX reverse proxy

server {
    listen 80;
    server_name search.example.com;

    location / {
        proxy_pass http://127.0.0.1:7700;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        client_max_body_size 20m;
        proxy_read_timeout 75s;
    }
}

If copy button does not work in your browser/editor, select the block and copy manually.

Enable and test NGINX:

sudo ln -s /etc/nginx/sites-available/meilisearch /etc/nginx/sites-enabled/meilisearch
sudo nginx -t
sudo systemctl reload nginx

If copy button does not work in your browser/editor, select the block and copy manually.

6) Lock down firewall

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo ufw status verbose

If copy button does not work in your browser/editor, select the block and copy manually.

7) Issue TLS certificate (recommended)

sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d search.example.com --redirect --agree-tos -m [email protected] -n

If copy button does not work in your browser/editor, select the block and copy manually.

8) Create scoped API keys instead of sharing the master key with applications.

export MEILI_URL='https://search.example.com'
export MASTER_KEY='replace_with_64_char_random_secret'

curl -sS "$MEILI_URL/keys"   -H "Authorization: Bearer $MASTER_KEY"   -H "Content-Type: application/json"   -d '{
        "description": "frontend-search-key",
        "actions": ["search"],
        "indexes": ["products", "docs"],
        "expiresAt": null
      }' | jq .

If copy button does not work in your browser/editor, select the block and copy manually.

9) Build an initial index and settings for typo tolerance and ranking behavior.

curl -sS "$MEILI_URL/indexes"   -H "Authorization: Bearer $MASTER_KEY"   -H "Content-Type: application/json"   -d '{"uid":"docs","primaryKey":"id"}' | jq .

curl -sS "$MEILI_URL/indexes/docs/settings"   -H "Authorization: Bearer $MASTER_KEY"   -H "Content-Type: application/json"   -d '{
        "searchableAttributes": ["title", "content", "tags"],
        "filterableAttributes": ["category", "published"],
        "sortableAttributes": ["updated_at"]
      }' | jq .

If copy button does not work in your browser/editor, select the block and copy manually.

This pattern keeps the platform secure while giving application teams explicit, least-privilege credentials and clear operational contracts.

Configuration and secrets handling best practices

  • Master key hygiene: store only in root-readable secret files or a vault-backed runtime injector; never commit to repo.
  • Split keys by use case: one key for frontend search-only, another for indexing jobs, and a short-lived key for admin tasks.
  • Rotate keys safely: create new keys, update clients, then revoke old keys after verification windows.
  • Constrain payload size: keep NGINX client_max_body_size aligned with expected batch-index operations.
  • Protect backups: snapshots can contain sensitive data; encrypt-at-rest in object storage and enforce retention policies.

For CI/CD, inject secrets at deploy time and keep Compose templates environment-agnostic. Teams commonly fail by mixing environment values inside tracked YAML; instead, maintain one Compose template and environment files per stage, each managed by access controls and rotation policies.

Verification checklist

Run these checks after first deploy and after each upgrade:

# Health endpoint
curl -sS https://search.example.com/health | jq .

# Auth required check (should return 401 without key)
curl -i https://search.example.com/indexes

# Authenticated list indexes
curl -sS https://search.example.com/indexes   -H "Authorization: Bearer $MASTER_KEY" | jq .

# Ingest sample doc and query
curl -sS -X POST https://search.example.com/indexes/docs/documents   -H "Authorization: Bearer $MASTER_KEY"   -H "Content-Type: application/json"   -d '[{"id":1,"title":"Meilisearch runbook","content":"production checklist","published":true}]' | jq .

curl -sS -X POST https://search.example.com/indexes/docs/search   -H "Authorization: Bearer $MASTER_KEY"   -H "Content-Type: application/json"   -d '{"q":"runbook"}' | jq .

If copy button does not work in your browser/editor, select the block and copy manually.

Operational acceptance criteria:

  • p95 search latency remains within your SLO under expected concurrency.
  • No anonymous access to privileged routes.
  • Snapshot and restore are tested, not just configured.
  • Logs and metrics are centralized for proactive alerting.

Common issues and fixes

1) 502 from NGINX after reboot

Cause: NGINX starts before container is healthy. Fix: add service ordering checks and verify Docker auto-restart. Confirm upstream with curl http://127.0.0.1:7700/health.

2) Indexing jobs timeout on large batches

Cause: payload too large or slow disks. Fix: reduce batch size, tune proxy_read_timeout, and move data volume to faster storage.

3) Search misses expected fields

Cause: fields not in searchableAttributes. Fix: review index settings and reindex after schema changes.

4) Accidental key leakage in logs

Cause: debug output includes headers/environment dumps. Fix: sanitize logs, rotate keys immediately, and tighten observability redaction.

5) Slow startup after unclean shutdown

Cause: recovery work on larger datasets. Fix: enforce graceful stop timeouts and test crash recovery during maintenance windows.

6) Snapshot restore mismatch with app schema

Cause: app release expects newer index structure. Fix: version index settings with release artifacts and run compatibility checks before rollback.

FAQ

Do I need to expose port 7700 publicly?

No. Keep Meilisearch private and expose only NGINX over 443. This simplifies firewall policy and gives you consistent TLS/security controls.

How often should I rotate Meilisearch keys?

For production, rotate administrative keys on a fixed cadence (for example every 30–90 days) and immediately after personnel or incident events.

Can I run this stack without HTTPS during initial setup?

You can for local bootstrap, but production traffic should enforce HTTPS with automatic redirect and certificate renewal monitoring.

What backup approach is safest for fast recovery?

Use regular snapshots to off-host object storage with retention tiers (daily/weekly) and run restore drills so recovery time is measured, not assumed.

When should we move from single-node to clustered search?

If your write volume, index size, or availability requirements exceed single-node boundaries, plan migration to a distributed architecture with staged cutover tests.

How do we prevent schema drift between app and search index?

Version index settings alongside application releases, validate in staging, and gate production deploys on successful index migration checks.

What’s the minimum monitoring to start with?

Collect container health, query latency, indexing queue timings, disk usage, and HTTP error rates. Alert on sustained latency and disk saturation trends.

Related guides

Talk to us

If you want help designing a zero-trust rollout, hardening your Kubernetes control plane, or building an operations runbook your team can actually execute during incidents, we can help.

Contact Us

Production Guide: Deploy Grafana Loki + Promtail with Docker Compose + Traefik + Let's Encrypt on Ubuntu
A production-ready Loki stack with secure ingress, retention controls, and operator-focused verification steps.