Tutorials / Backend integrations / Self-hosted search with Meilisearch
πŸ“ Written ● Intermediate Updated 2026-05-13

Self-hosted search with Meilisearch

Meilisearch is what happens when someone writes Algolia's user-facing model as a single Rust binary. Run it on a $5 VPS, index records via HTTP, query in milliseconds. Open source; same UI library as Algolia.

When to pick Meilisearch

0
  • Pick Meilisearch when β€” you want Algolia-shaped DX at a fraction of the cost, your data shouldn't leave your infrastructure, or you've outgrown Algolia's free tier and don't want to start paying.
  • Pick Algolia for zero ops + global edge by default.
  • Pick Typesense β†— for the same shape as Meilisearch with slightly different opinions.
  • Pick Postgres FTS if you have under ~100K records and search doesn't need to be flagship.

Two ways to run Meilisearch: self-host (free, you run the box) or Meilisearch Cloud β†— (paid, they run it). This tutorial covers self-hosting.

Run Meilisearch locally

1

Single binary:

curl -L https://install.meilisearch.com | sh
./meilisearch --master-key="your-strong-master-key"

Listens on http://localhost:7700. The master key is required in production; without one, Meilisearch starts in dev mode (no auth). Generate a long random string for real use.

Docker alternative:

docker run -it --rm \
  -p 7700:7700 \
  -v $(pwd)/meili_data:/meili_data \
  getmeili/meilisearch:latest \
  meilisearch --master-key="your-strong-master-key"

Test it:

curl http://localhost:7700/health
# {"status":"available"}

Self-hosting docs β†—.

Run on a production VPS

2

Spin up a $5/month box on DigitalOcean, Hetzner, etc. SSH in and run:

# Install Meilisearch
curl -L https://install.meilisearch.com | sh
sudo mv ./meilisearch /usr/local/bin/

# Run as a systemd service
sudo nano /etc/systemd/system/meilisearch.service

Paste:

[Unit]
Description=Meilisearch
After=systemd-user-sessions.service

[Service]
Type=simple
ExecStart=/usr/local/bin/meilisearch --config-file-path /etc/meilisearch.toml
Restart=on-failure

[Install]
WantedBy=multi-user.target

Create /etc/meilisearch.toml with your master key, data path, environment, etc. (see configuration docs β†—).

sudo systemctl enable meilisearch
sudo systemctl start meilisearch

Put Nginx + HTTPS in front. Now you have a real search endpoint at https://search.yourdomain.com.

Create API keys

3

The master key is for admin operations only β€” don't ship it. Use scoped API keys for your app.

Auto-generated on first run: query the keys endpoint with the master key:

curl -X GET 'http://localhost:7700/keys' \
  -H 'Authorization: Bearer your-master-key'

Two default keys are pre-generated:

  • Default Admin API Key β€” full read/write. Backend only.
  • Default Search API Key β€” search-only. Safe in browser.
MEILI_HOST=https://search.yourdomain.com
MEILI_MASTER_KEY=your-master-key                # backend admin ops only
MEILI_ADMIN_KEY=xxxxxxxxxx                      # backend; from Default Admin API Key
MEILI_SEARCH_KEY=xxxxxxxxxx                     # safe for client; from Default Search API Key

Install the SDK

4
npm install meilisearch

Official SDKs: JS, Python, Ruby, PHP, Go, Java, .NET, Rust, Swift, Dart β†—.

Index your first records

5
import { MeiliSearch } from "meilisearch";

const client = new MeiliSearch({
  host: process.env.MEILI_HOST,
  apiKey: process.env.MEILI_ADMIN_KEY,
});

// Add documents (creates the index automatically)
await client.index("posts").addDocuments([
  {
    id: 1,                                    // required; primary key
    title: "How to deploy to Vercel",
    body: "First, install the CLI...",
    tags: ["deploy", "vercel"],
    published_at: 1714521600,
  },
  { id: 2, title: "...", body: "..." },
]);

Meilisearch processes async β€” returns a task ID. For batch imports of millions of records, poll /tasks/{id} for completion. For small adds, results are searchable in milliseconds.

Configure the index

6

Two settings that matter most (mirroring Algolia):

// Which fields are searchable, in priority order
await client.index("posts").updateSearchableAttributes([
  "title",
  "body",
  "tags",
]);

// Which fields you can filter on
await client.index("posts").updateFilterableAttributes(["tags", "author"]);

// Custom ranking
await client.index("posts").updateRankingRules([
  "words", "typo", "proximity", "attribute",
  "sort", "exactness",
  "published_at:desc",       // newer first
]);

Settings docs β†—.

Search from the browser

7
import { MeiliSearch } from "meilisearch";

const client = new MeiliSearch({
  host: process.env.NEXT_PUBLIC_MEILI_HOST,
  apiKey: process.env.NEXT_PUBLIC_MEILI_SEARCH_KEY,    // search-only; safe
});

const results = await client.index("posts").search("vercel", {
  limit: 10,
  filter: ['tags = "deploy"'],
});

Use the search-only key. It can only read, not modify; safe to ship in browser.

Drop in the UI library

8

Meilisearch ships an InstantSearch-compatible adapter, so all the Algolia UI widgets work:

npm install instantsearch.js @meilisearch/instant-meilisearch
# or for React:
npm install react-instantsearch @meilisearch/instant-meilisearch
import { InstantSearch, SearchBox, Hits } from "react-instantsearch";
import { instantMeiliSearch } from "@meilisearch/instant-meilisearch";

const { searchClient } = instantMeiliSearch(
  process.env.NEXT_PUBLIC_MEILI_HOST,
  process.env.NEXT_PUBLIC_MEILI_SEARCH_KEY,
);

export function Search() {
  return (
    <InstantSearch searchClient={searchClient} indexName="posts">
      <SearchBox />
      <Hits hitComponent={({ hit }) => <div>{hit.title}</div>} />
    </InstantSearch>
  );
}

instant-meilisearch β†—.

Sync your DB β†’ Meilisearch

9

Same patterns as Algolia:

  • On every write β€” DB write also calls addDocuments. Simple, doubles latency slightly.
  • Async via Inngest β€” fire an event after DB write; let a function sync to Meilisearch. Decoupled.
  • Periodic batch β€” every N minutes, sync all changed records. Simplest; small staleness window.

For Postgres β†’ Meilisearch, Airbyte β†— has a free connector. Or use Supabase's Meilisearch wrapper β†— if you're on Supabase.

Common operations

10
// Update (merges with existing fields by primary key)
await client.index("posts").updateDocuments([
  { id: 1, body: "Updated body" },
]);

// Delete
await client.index("posts").deleteDocument(1);
await client.index("posts").deleteDocuments({ filter: 'tags = "draft"' });

// Stats
const stats = await client.index("posts").getStats();
console.log(stats.numberOfDocuments);

// Drop the whole index
await client.deleteIndex("posts");

Backups

11

Meilisearch produces a single dump file you can restore from:

curl -X POST 'http://localhost:7700/dumps' \
  -H 'Authorization: Bearer your-master-key'

Dumps land in your configured data path. Schedule via cron; rsync to S3 / R2 for offsite copies. Restore by starting Meilisearch with --import-dump path/to/dump.dump.

Meilisearch can rebuild from your source-of-truth DB in minutes β€” for many teams, "the DB is the backup; just re-sync to Meilisearch on disaster" works fine.

Use Meilisearch Cloud if self-hosting feels heavy

12

Meilisearch Cloud β†— hosts the same engine you'd self-host. $30/mo for the starter tier. Worth it if you don't want to maintain a Linux box; still much cheaper than Algolia at meaningful scale.

Migration is symmetric β€” dump from self-hosted, restore to Cloud, switch your client's host URL.

Don't expose Meilisearch without auth. Without a master key, anyone who can reach your port can read AND wipe your index. Always run with a strong master key in production. Bind to localhost + put Nginx in front, or whitelist client IPs at your firewall.

Official references

What's next