Skip to Content

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

A production-ready private search deployment with TLS, secret hygiene, snapshots, health checks, and rollback-ready operations.

Fast internal search becomes more valuable once your team has more runbooks, customer notes, documents, and application records than people can reliably browse by hand. Meilisearch is a practical fit for that job because it gives developers typo-tolerant search, simple API keys, and predictable indexing without the operational weight of a large search cluster. This guide shows a compact production deployment on Ubuntu using Docker Compose, Caddy for automatic HTTPS, a private master key, snapshots, and a restore path you can rehearse before the first incident.

The pattern is intentionally conservative: one host, one Meilisearch container, one Caddy reverse proxy, tight firewall rules, and filesystem backups. It is suitable for small business apps, internal portals, knowledge-base search, and staging environments that need real TLS and secret handling. For large multi-tenant platforms, treat this as the baseline and add managed block storage, external object-storage backups, alerting, and capacity tests before broad rollout.

Architecture and flow overview

Users and applications connect to https://search.example.com. Caddy terminates TLS, applies security headers, and proxies traffic over an isolated Docker network to Meilisearch on port 7700. Meilisearch stores indexes in a mounted data directory and writes snapshots to a separate mounted path. A nightly systemd timer requests a snapshot, archives configuration plus data, and prunes older archives.

The master key is generated once on the host and loaded from a permission-restricted .env file. Applications should not use the master key directly. After deployment, create scoped search and indexing keys for each application role, rotate them when teams change, and keep the master key reserved for operations. That single rule prevents a leaked frontend key from becoming full administrative access.

Prerequisites

  • Ubuntu 22.04 or 24.04 server with a non-root sudo user.
  • A DNS record such as search.example.com pointing to the server.
  • Ports 80 and 443 reachable from the public internet for Caddy certificate issuance.
  • At least 2 GB RAM for small indexes; plan more memory for heavy indexing or large document bodies.
  • A backup destination outside the server, even if the first version starts with local archives.

Step-by-step deployment

Start by patching the host, installing Docker Engine from the official repository, and enabling the firewall. Reconnect after adding your user to the Docker group, or run the remaining commands with sudo until your session picks up group membership.

sudo apt update && sudo apt -y upgrade
sudo apt -y install ca-certificates curl gnupg ufw
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt update
sudo apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker "$USER"

If the copy button is unavailable, select the command block and copy it manually.

Create the deployment directory, data paths, and secrets. The commands below generate a high-entropy master key and inject it into the environment file without printing it to the terminal. Keep .master_key and .env out of tickets, screenshots, and shared chat channels.

sudo mkdir -p /opt/meilisearch/{data,snapshots,backups}
sudo chown -R 1000:1000 /opt/meilisearch/data /opt/meilisearch/snapshots
cd /opt/meilisearch
umask 077
openssl rand -base64 48 | tr -d '
' > .master_key
printf '
' >> .master_key
cat > .env <<'EOF'
MEILI_ENV=production
MEILI_HTTP_ADDR=0.0.0.0:7700
MEILI_NO_ANALYTICS=true
MEILI_SNAPSHOT_DIR=/meili_snapshots
MEILI_DUMP_DIR=/meili_dumps
MEILI_DB_PATH=/meili_data
MEILI_EXPERIMENTAL_ENABLE_METRICS=true
MEILI_MASTER_KEY=replace-with-generated-key
EOF
python3 - <<'PY'
from pathlib import Path
env=Path('.env')
key=Path('.master_key').read_text().strip()
env.write_text(env.read_text().replace('replace-with-generated-key', key))
PY
chmod 600 .env .master_key

If the copy button is unavailable, select the command block and copy it manually.

Now define the two-container Compose stack. The Meilisearch image is pinned to a major/minor release line for predictable upgrades. Caddy sits in front of it so the search API is never exposed as a raw HTTP service on the internet.

cat > docker-compose.yml <<'EOF'
services:
  meilisearch:
    image: getmeili/meilisearch:v1.12
    restart: unless-stopped
    env_file: .env
    volumes:
      - ./data:/meili_data
      - ./snapshots:/meili_snapshots
      - ./dumps:/meili_dumps
    networks:
      - search_net
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--spider", "http://127.0.0.1:7700/health"]
      interval: 30s
      timeout: 5s
      retries: 5

  caddy:
    image: caddy:2.8
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - search_net

networks:
  search_net:
    driver: bridge

volumes:
  caddy_data:
  caddy_config:
EOF

If the copy button is unavailable, select the command block and copy it manually.

Replace search.example.com with the real hostname, validate the Compose file, open only the required ports, and start the stack. If Caddy cannot obtain a certificate, verify DNS first; do not work around the issue by exposing Meilisearch directly.

cat > Caddyfile <<'EOF'
search.example.com {
  encode zstd gzip
  header {
    Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
    X-Content-Type-Options "nosniff"
    Referrer-Policy "strict-origin-when-cross-origin"
    -Server
  }
  reverse_proxy meilisearch:7700
}
EOF
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
docker compose config
docker compose up -d

If the copy button is unavailable, select the command block and copy it manually.

Configuration and secrets handling best practices

Meilisearch security starts with key discipline. The master key should live only on the host and in your password manager. For applications, create separate tenant or workload keys with minimal actions. A public web application may only need search access to selected indexes, while a background worker may need document update privileges. Keeping those keys separate makes rotation smaller and incident response clearer.

Also decide what should never be indexed. Search systems often become accidental data aggregation layers because teams send whole records instead of a carefully selected search document. Avoid indexing raw secrets, private tokens, full payment data, or sensitive notes that are not needed for lookup. If you must support regulated or customer-sensitive content, document retention, deletion, and access-control behavior before launch.

Finally, keep operational files readable by administrators only. Review permissions on every deploy, restrict SSH, and centralize logs where possible. The deployment is simple, but simple does not mean casual: it is still an internet-facing API backed by business data.

Verification checklist

Before handing the endpoint to developers, verify the full path: DNS, TLS, proxying, authentication, index creation, document ingestion, and query behavior. The sample below creates a tiny index and confirms a result comes back through the public HTTPS endpoint.

export MEILI_MASTER_KEY=$(cat /opt/meilisearch/.master_key)
curl -fsS https://search.example.com/health
curl -fsS -H "Authorization: Bearer $MEILI_MASTER_KEY" https://search.example.com/version
curl -fsS -X POST https://search.example.com/indexes   -H "Authorization: Bearer $MEILI_MASTER_KEY"   -H 'Content-Type: application/json'   --data '{"uid":"runbooks","primaryKey":"id"}'
curl -fsS -X POST https://search.example.com/indexes/runbooks/documents   -H "Authorization: Bearer $MEILI_MASTER_KEY"   -H 'Content-Type: application/json'   --data '[{"id":1,"title":"Backup drill","team":"platform"}]'
curl -fsS -H "Authorization: Bearer $MEILI_MASTER_KEY"   'https://search.example.com/indexes/runbooks/search?q=backup' | jq .

If the copy button is unavailable, select the command block and copy it manually.

  • /health returns available through Caddy.
  • The version endpoint requires the bearer token for privileged access.
  • A test index can be created, populated, searched, and deleted.
  • docker compose ps shows both containers healthy or running.
  • Firewall rules expose SSH, HTTP, and HTTPS only.
  • Backups produce archives and those archives can be copied off host.

Backups and restore testing

Snapshots are not a substitute for off-host backups, but they are the right primitive for consistent Meilisearch recovery. The routine below requests a snapshot, packages snapshots, data, and configuration, and schedules the task nightly. In production, sync the resulting archive to object storage or a backup appliance immediately after creation.

cat > /opt/meilisearch/backup.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd /opt/meilisearch
export MEILI_MASTER_KEY=$(cat .master_key)
stamp=$(date -u +%Y%m%dT%H%M%SZ)
curl -fsS -X POST -H "Authorization: Bearer $MEILI_MASTER_KEY" http://127.0.0.1:7700/snapshots >/tmp/meili-snapshot.json
sleep 10
tar -C /opt/meilisearch -czf "backups/meili-${stamp}.tgz" snapshots data docker-compose.yml Caddyfile .env
find backups -type f -name 'meili-*.tgz' -mtime +14 -delete
EOF
chmod 750 /opt/meilisearch/backup.sh
cat > /etc/systemd/system/meilisearch-backup.service <<'EOF'
[Unit]
Description=Create Meilisearch backup archive
[Service]
Type=oneshot
ExecStart=/opt/meilisearch/backup.sh
EOF
cat > /etc/systemd/system/meilisearch-backup.timer <<'EOF'
[Unit]
Description=Run Meilisearch backup nightly
[Timer]
OnCalendar=*-*-* 03:20:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now meilisearch-backup.timer

If the copy button is unavailable, select the command block and copy it manually.

Run one restore drill before the service contains important data. A restore drill catches ownership problems, missing environment files, DNS assumptions, and version drift while the stakes are low. Keep a short recovery note beside the backup archives so the person on call does not have to reconstruct the process during an outage.

sudo systemctl stop meilisearch-backup.timer || true
cd /opt/meilisearch
docker compose down
sudo tar -xzf backups/meili-YYYYMMDDTHHMMSSZ.tgz -C /opt/meilisearch --overwrite
sudo chown -R 1000:1000 data snapshots
docker compose up -d
docker compose ps
curl -fsS -H "Authorization: Bearer $(cat .master_key)" https://search.example.com/health
sudo systemctl start meilisearch-backup.timer

If the copy button is unavailable, select the command block and copy it manually.

Common issues and fixes

Caddy cannot issue certificates. Confirm the DNS A record points to the server, ports 80 and 443 are open, and no other service is bound to those ports. Check docker compose logs caddy for the exact ACME error.

Search requests return unauthorized. Verify the bearer token, confirm there is no extra whitespace in the copied key, and make sure the application is using the right scoped key for the target index. Avoid embedding the master key in browser code.

Indexing is slow or memory usage spikes. Batch document updates, remove unnecessary fields, and schedule large imports away from peak traffic. If indexes keep growing, increase memory before the host starts swapping.

Restored data has permission errors. Reapply ownership to the mounted data and snapshot paths. Containers often expect a specific UID, and files restored as root can prevent the service from starting cleanly.

Queries return unexpected ranking. Review searchable attributes, displayed attributes, ranking rules, synonyms, and typo tolerance for the index. Treat ranking as product configuration, not just infrastructure.

FAQ

Can I run Meilisearch without Caddy?

You can, but you should not expose the raw HTTP service publicly. Caddy gives automatic TLS, simple reverse proxying, compression, and security headers with very little maintenance.

Should frontend code use the master key?

No. Frontend code should use a scoped search key with only the minimum indexes and actions it needs. Keep the master key for administrative operations on trusted systems.

How often should snapshots run?

Nightly is a reasonable starting point for internal search. If your index changes constantly or powers customer-facing workflows, run snapshots more often and replicate archives off host.

Can this deployment handle multiple applications?

Yes, create separate indexes and scoped keys per application. Name indexes consistently, document ownership, and avoid mixing unrelated tenants in the same index unless access rules are clear.

How do I upgrade safely?

Read the release notes, take a snapshot, copy the backup off host, test the target version in staging, then update the image tag and run docker compose up -d. Keep rollback notes ready.

What monitoring should I add next?

Track container health, disk usage, backup freshness, TLS expiry, request error rates, and indexing latency. If metrics are enabled, scrape them with your existing monitoring stack.

Internal links

Talk to us

If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.

Contact Us

Production Guide: Deploy PocketBase with systemd + NGINX + SQLite + UFW on Ubuntu
Run PocketBase as a hardened single-binary backend with TLS, backups, firewall rules, and operational checks.