Skip to Content

Production Guide: Deploy Meilisearch with Docker Compose + Caddy on Ubuntu

A production-first Meilisearch deployment with TLS, snapshots, health checks, backups, and practical runbooks.

Search quality often decides whether users convert or bounce. Teams usually discover this when product catalog search starts timing out, support articles become hard to find, or internal tooling depends on fast text lookup. This guide shows a practical, production-oriented way to deploy Meilisearch on Ubuntu using Docker Compose + Caddy, with HTTPS, service isolation, snapshot backups, operational checks, and failure recovery runbooks. The approach is designed for teams that need quick implementation without sacrificing security and maintainability.

Instead of a demo-only setup, we will build a deployment that can survive real operational pressure: restarts, disk growth, certificate renewals, misconfiguration, and routine upgrades. By the end, you will have a search service that is easy to operate and straightforward to hand off to another engineer.

Architecture and request flow overview

We will run two containers on a user-defined Docker network:

  • meilisearch: the search engine process, persisted to a mounted volume.
  • caddy: reverse proxy that terminates TLS, handles certificate renewal, and forwards traffic to Meilisearch.

Caddy exposes ports 80/443 publicly. Meilisearch remains private to the Docker network and is not mapped directly to the host. This minimizes accidental exposure and ensures all inbound traffic passes through TLS and proxy policy.

  • Client request β†’ https://search.example.com
  • Caddy validates TLS and applies proxy behavior
  • Request is forwarded to http://meilisearch:7700 on the private network
  • Meilisearch responds with indexed/search results

For operations, snapshots are written to a host-mounted backup path, while Meilisearch primary data remains in a dedicated data volume. This separation makes rollback and migration simpler.

Prerequisites

  • Ubuntu 22.04/24.04 server with at least 2 vCPU, 4 GB RAM, and SSD storage.
  • A DNS A record for your search hostname (for example, search.example.com) pointing to the server.
  • Docker Engine + Docker Compose plugin installed.
  • Firewall allowing inbound 80 and 443; SSH restricted to trusted sources.
  • A strong Meilisearch master key (32+ random chars) stored outside source control.

Step 1: Prepare host directories and environment files

sudo mkdir -p /opt/meilisearch/{data,snapshots,caddy}
sudo chown -R $USER:$USER /opt/meilisearch
cd /opt/meilisearch
openssl rand -base64 48 | tr -d '
' > .meili_master_key
chmod 600 .meili_master_key

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

Use separate directories for data and snapshots. Data is the live index store; snapshots are point-in-time backups used for disaster recovery and migration testing.

Step 2: Create Docker Compose configuration

cat > /opt/meilisearch/docker-compose.yml <<'YAML'
services:
  meilisearch:
    image: getmeili/meilisearch:v1.11
    container_name: meilisearch
    restart: unless-stopped
    environment:
      - MEILI_MASTER_KEY_FILE=/run/secrets/meili_master_key
      - MEILI_ENV=production
      - MEILI_NO_ANALYTICS=true
      - MEILI_SCHEDULE_SNAPSHOT=true
      - MEILI_SNAPSHOT_DIR=/meili_snapshots
      - MEILI_HTTP_ADDR=0.0.0.0:7700
      - MEILI_LOG_LEVEL=INFO
    volumes:
      - /opt/meilisearch/data:/meili_data
      - /opt/meilisearch/snapshots:/meili_snapshots
    secrets:
      - meili_master_key
    networks:
      - search_net
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:7700/health"]
      interval: 20s
      timeout: 5s
      retries: 6

  caddy:
    image: caddy:2.8
    container_name: meili_caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /opt/meilisearch/Caddyfile:/etc/caddy/Caddyfile:ro
      - /opt/meilisearch/caddy:/data
    depends_on:
      - meilisearch
    networks:
      - search_net

secrets:
  meili_master_key:
    file: /opt/meilisearch/.meili_master_key

networks:
  search_net:
    driver: bridge
YAML

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

Step 3: Configure Caddy for TLS and proxying

cat > /opt/meilisearch/Caddyfile <<'CADDY'
search.example.com {
    encode zstd gzip

    @health path /health
    handle @health {
        reverse_proxy meilisearch:7700
    }

    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"
    }

    reverse_proxy meilisearch:7700 {
        health_uri /health
        health_interval 15s
        health_timeout 3s
    }

    log {
        output stdout
        format console
    }
}
CADDY

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

Replace search.example.com with your real domain before startup. Caddy will request certificates automatically once DNS and firewall are correct.

Step 4: Start services and verify baseline health

cd /opt/meilisearch
docker compose pull
docker compose up -d
docker compose ps
curl -sS https://search.example.com/health

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

A healthy response should include {"status":"available"}. If it fails, check container logs first before changing config.

Step 5: Secrets handling and API key strategy

Do not hardcode master keys in application repos or CI logs. For production, use one of these patterns:

  • Runtime file-mounted secret (used in this guide) with root-only permissions.
  • External secret manager (Vault, cloud secret service) injected at deploy time.
  • Rotated environment secrets delivered by your orchestration pipeline.

Use the master key only for admin and initial key creation. Generate scoped search keys for applications and rotate them on a schedule. Limit index permissions so a compromised app key cannot alter unrelated indexes.

MEILI_KEY=$(cat /opt/meilisearch/.meili_master_key)
curl -sS https://search.example.com/keys   -H "Authorization: Bearer ${MEILI_KEY}"

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

Step 6: Create index, ingest sample data, and test relevance

MEILI_KEY=$(cat /opt/meilisearch/.meili_master_key)

curl -sS -X POST https://search.example.com/indexes   -H "Authorization: Bearer ${MEILI_KEY}"   -H "Content-Type: application/json"   --data '{"uid":"docs","primaryKey":"id"}'

curl -sS -X POST https://search.example.com/indexes/docs/documents   -H "Authorization: Bearer ${MEILI_KEY}"   -H "Content-Type: application/json"   --data '[
    {"id":1,"title":"Reset MFA for users","body":"Step-by-step guide for identity admins"},
    {"id":2,"title":"Deploy API gateway","body":"Production checklist and rollout plan"},
    {"id":3,"title":"PostgreSQL backup drills","body":"Restore testing and PITR validation"}
  ]'

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

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

In real environments, tune ranking rules, synonyms, stop words, typo tolerance, and searchable/filterable attributes according to your product domain. Treat search configuration as versioned infrastructureβ€”not ad-hoc edits.

Step 7: Backups, snapshots, and restore drill

Backups are useful only if restores are tested. Schedule snapshot verification and run at least one restore drill per quarter. Keep immutable off-host copies (object storage, backup server, or encrypted vault).

ls -lah /opt/meilisearch/snapshots
rsync -avz /opt/meilisearch/snapshots/ backupuser@backuphost:/srv/backups/meilisearch/
docker compose down
cp /opt/meilisearch/snapshots/data.ms /opt/meilisearch/data/data.ms
docker compose up -d

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

Document Recovery Time Objective (RTO) and Recovery Point Objective (RPO). These two values should drive snapshot frequency, retention policy, and storage class decisions.

Step 8: Operational verification checklist

  • Availability: /health returns available over HTTPS.
  • TLS: certificate is valid, auto-renew enabled, and HSTS active.
  • Persistence: test document survives container restart.
  • Performance: p95 query latency within target under expected load.
  • Security: only 80/443 publicly exposed; 7700 not host-bound.
  • Backups: latest snapshot copied off-host and restore-tested.
docker compose restart meilisearch
sleep 5
curl -sS https://search.example.com/health
docker compose logs --tail=80 meilisearch
docker compose logs --tail=80 caddy

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

Common issues and practical fixes

Issue: TLS certificate is not issued

Cause: DNS not pointed correctly or inbound 80/443 blocked.

Fix: validate DNS A record, open firewall, and recheck Caddy logs for ACME details.

Issue: 502/504 from Caddy

Cause: Meilisearch container unhealthy, wrong upstream name, or startup race.

Fix: verify docker compose ps, network membership, and health endpoint response inside container network.

Issue: Search latency grows as data increases

Cause: under-provisioned CPU/RAM or unoptimized index settings.

Fix: tune index settings, add resources, benchmark with representative traffic, and avoid noisy-neighbor workloads on shared hosts.

Issue: Keys leaked in shell history

Cause: using inline environment exports in interactive sessions.

Fix: use secret files, restricted permissions, and session hygiene; rotate compromised keys immediately.

Issue: Backups exist but restore fails

Cause: snapshot mismatch, corruption, or untested restore sequence.

Fix: run periodic restore drills in staging and keep clear runbooks with version notes.

FAQ

1) Is Meilisearch suitable for production workloads?

Yes, when you pair it with disciplined operations: HTTPS, key management, backups, and monitoring. The engine is fast, but reliability depends on operational practices around it.

2) Should I expose port 7700 directly to the internet?

No. Keep Meilisearch on a private Docker network and expose only Caddy on 80/443. This centralizes TLS and security headers while reducing accidental exposure.

3) How often should I create snapshots?

It depends on your RPO. For frequently changing datasets, schedule snapshots more often and move them off-host. The key requirement is proving restore reliability through drills.

4) Can I run multiple indexes for different teams?

Yes. Use separate indexes and scoped API keys. This simplifies permission boundaries and allows team-specific relevance tuning.

5) How do I handle upgrades safely?

Pin image versions, clone to staging, snapshot before upgrade, run smoke tests, and only then promote. Keep rollback steps documented and validated.

6) What monitoring should I start with?

At minimum: uptime checks on /health, TLS expiry checks, container restarts, disk growth on data/snapshots, and query latency tracking from the app side.

7) Is Docker Compose enough, or should I move to Kubernetes?

Compose is often enough for single-node or small-team environments. Move to Kubernetes when you need stronger orchestration guarantees, broader platform standardization, or multi-service scaling policies.

Related internal guides

Talk to us

Need help deploying production-ready search infrastructure, implementing secure key workflows, or building backup and upgrade runbooks your team can trust? We can help with architecture, hardening, migration, and operational readiness.

Contact Us

Production Guide: Deploy Docmost with Docker Compose and HAProxy on Ubuntu
A production-oriented Docmost setup with TLS, secrets hygiene, health checks, backups, and operational troubleshooting.