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
sudoprivileges. - For the HTTPS step: a domain name such as
search.your_domainwith a DNS A record pointing toyour_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_keyormissing_authorization_header: check theAuthorization: Bearerheader and thatMEILI_MASTER_KEYis exported in the current shell.invalid_search_filterorinvalid_search_sort: the field is not infilterableAttributesorsortableAttributes, or the settings task is still running.- New documents do not appear in results: the indexing task may still be
enqueuedorprocessing, or it failed. List recent tasks withcurl -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: raiseclient_max_body_sizein 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.
