Meilisearch is an open source search engine written in Rust that returns typo-tolerant, relevant results as the user types, with very little configuration. In this tutorial you will install Meilisearch on Ubuntu 24.04 as a systemd service running under its own user, add documents, adjust relevancy, filter and sort results, create a search-only API key, and publish the API over HTTPS with Nginx.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS (x86_64), for example a CubePath VPS, with at least 1 GB of RAM. Indexing is memory and CPU intensive, so larger datasets benefit from 2 GB or more and SSD storage.
  • A non-root user with sudo privileges.
  • For the HTTPS step: a domain name such as search.your_domain with a DNS A record pointing to your_server_ip.

Step 1 - Installing the Meilisearch binary

Meilisearch is a single static binary. Install curl and jq (used to read JSON responses), then download the Linux build from the official GitHub releases. This guide uses v1.54.0; check the releases page for the current version:

sudo apt update
sudo apt install -y curl jq
curl -fL -o meilisearch https://github.com/meilisearch/meilisearch/releases/download/v1.54.0/meilisearch-linux-amd64

Install it to /usr/local/bin with the right permissions:

sudo install -m 0755 meilisearch /usr/local/bin/meilisearch
rm meilisearch

Check the version:

meilisearch --version
meilisearch 1.54.0

Step 2 - Creating a user and the configuration

Run Meilisearch under a dedicated system user that owns the data directory:

sudo useradd --system --home-dir /var/lib/meilisearch --shell /usr/sbin/nologin meilisearch
sudo install -d -o meilisearch -g meilisearch -m 0750 /var/lib/meilisearch /var/lib/meilisearch/data /var/lib/meilisearch/dumps

In production mode Meilisearch refuses to start without a master key of at least 16 bytes. Generate a random one:

openssl rand -hex 32

Create an environment file for the service:

sudo nano /etc/meilisearch.env

Paste the following, replacing your_master_key with the value you just generated:

MEILI_ENV=production
MEILI_MASTER_KEY=your_master_key
MEILI_HTTP_ADDR=127.0.0.1:7700
MEILI_DB_PATH=/var/lib/meilisearch/data
MEILI_DUMP_DIR=/var/lib/meilisearch/dumps
MEILI_NO_ANALYTICS=true

MEILI_HTTP_ADDR keeps the API on localhost; Nginx will publish it in Step 8. MEILI_ENV=production also disables the built-in search preview page. Restrict the file, since the master key grants full control:

sudo chown root:meilisearch /etc/meilisearch.env
sudo chmod 0640 /etc/meilisearch.env

Step 3 - Running Meilisearch with systemd

Create a unit file:

sudo nano /etc/systemd/system/meilisearch.service
[Unit]
Description=Meilisearch search engine
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=meilisearch
Group=meilisearch
WorkingDirectory=/var/lib/meilisearch
EnvironmentFile=/etc/meilisearch.env
ExecStart=/usr/local/bin/meilisearch
Restart=on-failure
RestartSec=5
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

Reload systemd, then enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now meilisearch
sudo systemctl status meilisearch
● meilisearch.service - Meilisearch search engine
     Loaded: loaded (/etc/systemd/system/meilisearch.service; enabled; preset: enabled)
     Active: active (running) since ...

Check the health endpoint, which does not need a key:

curl -s http://127.0.0.1:7700/health
{"status":"available"}

Export the master key into your shell session so the next commands can use it:

export MEILI_MASTER_KEY=$(sudo awk -F= '/^MEILI_MASTER_KEY/ {print $2}' /etc/meilisearch.env)

Step 4 - Adding documents

Meilisearch creates an index automatically the first time you add documents to it. Each document needs a unique primary key; here it is id. Create a sample file:

nano movies.json
[
  {"id": 1, "title": "The Dark Knight", "overview": "Batman faces the Joker, a criminal who wants to plunge Gotham into anarchy.", "genres": ["Action", "Crime", "Drama"], "release_year": 2008, "rating": 9.0},
  {"id": 2, "title": "Inception", "overview": "A thief who steals secrets through dream-sharing technology is given a final job.", "genres": ["Action", "Sci-Fi", "Thriller"], "release_year": 2010, "rating": 8.8},
  {"id": 3, "title": "Interstellar", "overview": "A team of explorers travels through a wormhole in space to save humanity.", "genres": ["Adventure", "Drama", "Sci-Fi"], "release_year": 2014, "rating": 8.7},
  {"id": 4, "title": "The Martian", "overview": "An astronaut stranded on Mars must survive until a rescue mission arrives.", "genres": ["Adventure", "Drama", "Sci-Fi"], "release_year": 2015, "rating": 8.1},
  {"id": 5, "title": "Heat", "overview": "A detective hunts a crew of professional bank robbers in Los Angeles.", "genres": ["Action", "Crime", "Drama"], "release_year": 1995, "rating": 8.3}
]

Send the file to the movies index:

curl -s -X POST "http://127.0.0.1:7700/indexes/movies/documents?primaryKey=id" \
  -H "Authorization: Bearer ${MEILI_MASTER_KEY}" \
  -H "Content-Type: application/json" \
  --data-binary @movies.json | jq
{
  "taskUid": 0,
  "indexUid": "movies",
  "status": "enqueued",
  "type": "documentAdditionOrUpdate",
  "enqueuedAt": "2026-09-25T10:00:00.000000Z"
}

Writes in Meilisearch are asynchronous: the API queues a task and returns immediately. Check that the task finished, using the taskUid from the response:

curl -s http://127.0.0.1:7700/tasks/0 \
  -H "Authorization: Bearer ${MEILI_MASTER_KEY}" | jq '.status, .details'
"succeeded"
{
  "receivedDocuments": 5,
  "indexedDocuments": 5
}

If status is failed, the error field explains why. The same endpoint also accepts NDJSON (Content-Type: application/x-ndjson) and CSV (text/csv) for large imports. Sending a document with an existing id via PUT updates only the fields you send.

Step 5 - Searching and tuning relevancy

Run a search with a typo in the query:

curl -s -X POST http://127.0.0.1:7700/indexes/movies/search \
  -H "Authorization: Bearer ${MEILI_MASTER_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"q": "interstelar"}' | jq '[.hits[].title], .processingTimeMs'
[
  "Interstellar"
]
1

By default every field is searchable with the same weight. Tell Meilisearch which fields to search and in which order of importance, and add synonyms. Settings changes are also tasks:

curl -s -X PATCH http://127.0.0.1:7700/indexes/movies/settings \
  -H "Authorization: Bearer ${MEILI_MASTER_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "searchableAttributes": ["title", "overview", "genres"],
    "synonyms": {
      "sci-fi": ["science fiction"],
      "science fiction": ["sci-fi"]
    }
  }' | jq '.taskUid'

With title first, a match in the title now ranks above a match in the overview. Useful search parameters for the response itself are limit and offset for pagination, attributesToRetrieve to return fewer fields, and attributesToHighlight to wrap matches in <em> tags:

curl -s -X POST http://127.0.0.1:7700/indexes/movies/search \
  -H "Authorization: Bearer ${MEILI_MASTER_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "q": "space",
    "limit": 5,
    "attributesToRetrieve": ["id", "title"],
    "attributesToHighlight": ["overview"]
  }' | jq '.hits[0]'

Step 6 - Filtering, sorting and facets

Fields must be declared filterable or sortable before you can use them in filter or sort. Update the settings:

curl -s -X PATCH http://127.0.0.1:7700/indexes/movies/settings \
  -H "Authorization: Bearer ${MEILI_MASTER_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "filterableAttributes": ["genres", "release_year", "rating"],
    "sortableAttributes": ["release_year", "rating"]
  }' | jq '.taskUid'

Wait until that task shows succeeded (check it with /tasks/<taskUid> as before), then search for science fiction released since 2010, sorted by rating, and ask for facet counts:

curl -s -X POST http://127.0.0.1:7700/indexes/movies/search \
  -H "Authorization: Bearer ${MEILI_MASTER_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "q": "",
    "filter": "genres = \"Sci-Fi\" AND release_year >= 2010",
    "sort": ["rating:desc"],
    "facets": ["genres"]
  }' | jq '[.hits[] | "\(.title) \(.rating)"], .facetDistribution'
[
  "Inception 8.8",
  "Interstellar 8.7",
  "The Martian 8.1"
]
{
  "genres": {
    "Action": 1,
    "Adventure": 2,
    "Drama": 2,
    "Sci-Fi": 3,
    "Thriller": 1
  }
}

An empty q returns every document that matches the filter. Filters support =, !=, >, >=, <, <=, TO ranges (rating 8 TO 9), IN [...], NOT, AND, OR and parentheses. facetDistribution gives the counts you need to draw a filter sidebar.

Step 7 - Creating a search-only API key

The master key should only be used for administration. When a master key is set, Meilisearch also creates a default search key and a default admin key. List them:

curl -s http://127.0.0.1:7700/keys \
  -H "Authorization: Bearer ${MEILI_MASTER_KEY}" | jq '.results[] | {name, actions, key}'

For a frontend, create a dedicated key limited to searching the movies index:

curl -s -X POST http://127.0.0.1:7700/keys \
  -H "Authorization: Bearer ${MEILI_MASTER_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Frontend search",
    "description": "Search-only key for the movies index",
    "actions": ["search"],
    "indexes": ["movies"],
    "expiresAt": null
  }' | jq '{uid, key}'

Test that this key can search but cannot add documents:

curl -s -o /dev/null -w "%{http_code}\n" -X POST http://127.0.0.1:7700/indexes/movies/documents \
  -H "Authorization: Bearer your_search_key" \
  -H "Content-Type: application/json" \
  -d '[{"id": 99, "title": "Test"}]'
403

For multi-tenant applications, your backend can sign short-lived tenant tokens from a search key with a forced filter (for example user_id = 42) using the official SDKs, so users only see their own documents.

Step 8 - Publishing the API over HTTPS with Nginx

Install Nginx and Certbot:

sudo apt install -y nginx certbot python3-certbot-nginx

Create a server block, replacing search.your_domain:

sudo nano /etc/nginx/sites-available/meilisearch
server {
    listen 80;
    listen [::]:80;
    server_name search.your_domain;

    client_max_body_size 100m;

    location / {
        proxy_pass http://127.0.0.1:7700;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

client_max_body_size 100m matches the default payload limit of Meilisearch, so large document batches are not rejected by Nginx. Enable the site and open the firewall:

sudo ln -s /etc/nginx/sites-available/meilisearch /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

Request a Let's Encrypt certificate:

sudo certbot --nginx -d search.your_domain

From your local machine, search through HTTPS with the search-only key:

curl -s "https://search.your_domain/indexes/movies/search?q=mars" \
  -H "Authorization: Bearer your_search_key"

The response should contain "The Martian".

Backups and upgrades

A dump is a portable export of all indexes, documents and settings. Create one before every upgrade:

curl -s -X POST http://127.0.0.1:7700/dumps \
  -H "Authorization: Bearer ${MEILI_MASTER_KEY}" | jq '.taskUid'

When the task succeeds, the .dump file is in /var/lib/meilisearch/dumps. Copy it off the server. Read the release notes before upgrading: some versions can migrate the database in place, and for others you start the new binary with --import-dump pointing to the dump file.

Troubleshooting

  • The service exits immediately: run sudo journalctl -u meilisearch -n 50. In production mode, a missing or too short master key is the usual cause.
  • invalid_api_key or missing_authorization_header: check the Authorization: Bearer header and that MEILI_MASTER_KEY is exported in the current shell.
  • invalid_search_filter or invalid_search_sort: the field is not in filterableAttributes or sortableAttributes, or the settings task is still running.
  • New documents do not appear in results: the indexing task may still be enqueued or processing, or it failed. List recent tasks with curl -s "http://127.0.0.1:7700/tasks?limit=5" -H "Authorization: Bearer ${MEILI_MASTER_KEY}" | jq '.results[] | {uid, status, error}'.
  • 413 Request Entity Too Large: raise client_max_body_size in Nginx or split the import into smaller batches.

Conclusion

You installed Meilisearch on Ubuntu 24.04 as a hardened systemd service, indexed documents, tuned searchable attributes and synonyms, used filters, sorting and facets, created a search-only key and published the API over HTTPS. Next, connect your application with one of the official SDKs or the instant search UI libraries, schedule regular dumps or snapshots, and add ranking rules based on your own fields, such as popularity, once you know how users search.