Meilisearch is an open-source search engine written in Rust that returns typo-tolerant, relevance-ranked results in milliseconds through a simple REST API. It is a common choice for site search, product catalogs and documentation. In this tutorial you will install Meilisearch on Ubuntu 24.04 as a systemd service running under its own user, protect it with a master key, publish it over HTTPS behind Nginx, and then create an index, add documents, configure filters and generate a search-only API key.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, such as a CubePath VPS, with at least 1 GB of RAM. Meilisearch keeps its indexes on disk and memory-maps them, so more RAM means faster searches on large datasets.
  • A non-root user with sudo privileges.
  • A domain name with an A record pointing to your server's IP. This guide uses search.your_domain.
  • UFW enabled with OpenSSH allowed.

Step 1 - Downloading the Meilisearch binary

Meilisearch ships as a single static binary. Download the latest release for your architecture from GitHub (use meilisearch-linux-aarch64 instead on ARM servers):

curl -fsSL -o meilisearch https://github.com/meilisearch/meilisearch/releases/latest/download/meilisearch-linux-amd64

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

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

Verify it runs:

meilisearch --version
meilisearch 1.x.x

Step 2 - Creating a user and data directories

Run Meilisearch under a dedicated system user without a login shell, so a compromise of the service does not give access to the rest of the system:

sudo useradd --system --home-dir /var/lib/meilisearch --shell /usr/sbin/nologin meilisearch

Create the directories for the database, dumps and snapshots, owned by that user:

sudo install -d -o meilisearch -g meilisearch -m 0750 /var/lib/meilisearch /var/lib/meilisearch/data /var/lib/meilisearch/dumps /var/lib/meilisearch/snapshots

Step 3 - Writing the configuration file

Meilisearch reads its settings from a TOML file. First generate a random master key. In production mode Meilisearch refuses to start without a master key of at least 16 bytes:

openssl rand -hex 32

Copy the output, then create the configuration file:

sudo nano /etc/meilisearch.toml

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

env = "production"
master_key = "your_master_key"
http_addr = "127.0.0.1:7700"
db_path = "/var/lib/meilisearch/data"
dump_dir = "/var/lib/meilisearch/dumps"
snapshot_dir = "/var/lib/meilisearch/snapshots"
  • env = "production" requires the master key and disables the built-in search preview page.
  • http_addr binds only to localhost; Nginx will be the public entry point.

The file contains a secret, so make it readable only by root and the service user:

sudo chown root:meilisearch /etc/meilisearch.toml
sudo chmod 640 /etc/meilisearch.toml

Step 4 - Running Meilisearch as a systemd service

Create a unit file:

sudo nano /etc/systemd/system/meilisearch.service
[Unit]
Description=Meilisearch
Documentation=https://www.meilisearch.com/docs
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=meilisearch
Group=meilisearch
WorkingDirectory=/var/lib/meilisearch
ExecStart=/usr/local/bin/meilisearch --config-file-path /etc/meilisearch.toml
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/meilisearch

[Install]
WantedBy=multi-user.target

Load the unit, then enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now meilisearch

Check that it is running and listening only on localhost:

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

The /health route is public. Every other route needs a key. Store the master key in a shell variable for the rest of this session (the leading space keeps it out of your shell history in the default Ubuntu Bash configuration):

 export MEILI_MASTER_KEY='your_master_key'
curl -s http://127.0.0.1:7700/version -H "Authorization: Bearer $MEILI_MASTER_KEY"
{"commitSha":"...","commitDate":"...","pkgVersion":"1.x.x"}

Step 5 - Publishing Meilisearch over HTTPS with Nginx

Install Nginx and Certbot:

sudo apt update
sudo apt install nginx certbot python3-certbot-nginx

Create a server block for 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 matches Meilisearch's default payload limit of 100 MB, so large document batches are not rejected by Nginx first. Enable the site, test the configuration and reload:

sudo ln -s /etc/nginx/sites-available/meilisearch /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Open HTTP and HTTPS in the firewall and request a Let's Encrypt certificate. Certbot edits the server block to add TLS and an HTTP to HTTPS redirect:

sudo ufw allow 'Nginx Full'
sudo certbot --nginx -d search.your_domain

Test from your local machine:

curl https://search.your_domain/health
{"status":"available"}

Step 6 - Creating an index and adding documents

An index is a collection of documents with the same kind of data, similar to a table. Each document needs a unique primary key. Create a products index with id as its primary key:

curl -s -X POST http://127.0.0.1:7700/indexes \
  -H "Authorization: Bearer $MEILI_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"uid": "products", "primaryKey": "id"}'

Create a small JSON file with sample documents:

nano products.json
[
  {"id": 1, "name": "Wireless Mouse", "description": "Ergonomic wireless mouse with USB receiver", "category": "accessories", "price": 29.99, "in_stock": true},
  {"id": 2, "name": "Mechanical Keyboard", "description": "Tenkeyless mechanical keyboard with hot-swappable switches", "category": "keyboards", "price": 149.99, "in_stock": true},
  {"id": 3, "name": "USB-C Hub", "description": "7-in-1 hub with HDMI, USB 3.0 and SD card reader", "category": "accessories", "price": 59.99, "in_stock": false},
  {"id": 4, "name": "Wireless Keyboard", "description": "Slim wireless keyboard with quiet keys", "category": "keyboards", "price": 49.99, "in_stock": true}
]

Send the file to the index:

curl -s -X POST http://127.0.0.1:7700/indexes/products/documents \
  -H "Authorization: Bearer $MEILI_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  --data-binary @products.json
{"taskUid":1,"indexUid":"products","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2026-09-25T10:20:11.123Z"}

Write operations are asynchronous: Meilisearch returns a task and processes it in the background. Check the task using the taskUid from the response:

curl -s http://127.0.0.1:7700/tasks/1 -H "Authorization: Bearer $MEILI_MASTER_KEY"

When "status" is "succeeded", the documents are searchable. If it is "failed", the error field explains why (for example, a document without an id).

Step 7 - Searching and filtering

Run a search with a deliberate typo:

curl -s -X POST http://127.0.0.1:7700/indexes/products/search \
  -H "Authorization: Bearer $MEILI_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"q": "wirelss keybord"}'
{"hits":[{"id":4,"name":"Wireless Keyboard",...},{"id":1,"name":"Wireless Mouse",...}],"query":"wirelss keybord","processingTimeMs":1,"limit":20,"offset":0,"estimatedTotalHits":2}

Typo tolerance ranks "Wireless Keyboard" first without any configuration. The mouse also appears because, by default, Meilisearch drops query words from the end when not every word matches. To filter or sort by a field, you must declare it in the index settings first. Make category, price and in_stock filterable and price sortable:

curl -s -X PATCH http://127.0.0.1:7700/indexes/products/settings \
  -H "Authorization: Bearer $MEILI_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  --data '{
    "filterableAttributes": ["category", "price", "in_stock"],
    "sortableAttributes": ["price"],
    "searchableAttributes": ["name", "description", "category"]
  }'

searchableAttributes also sets the priority order: a match in name ranks above a match in description. Changing settings re-indexes the data, so wait for the returned task to succeed, then search for in-stock products under 100, cheapest first:

curl -s -X POST http://127.0.0.1:7700/indexes/products/search \
  -H "Authorization: Bearer $MEILI_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"q": "wireless", "filter": "in_stock = true AND price < 100", "sort": ["price:asc"], "facets": ["category"]}'

The response contains only the mouse and the wireless keyboard, sorted by price, plus a facetDistribution object with the number of hits per category, which you can use to build filter checkboxes in a UI.

Step 8 - Creating a search-only API key

Never put the master key in a browser or mobile app. Meilisearch creates two default keys (a search key and an admin key), and you can create more with limited permissions. Create a key that can only search the products index:

curl -s -X POST http://127.0.0.1:7700/keys \
  -H "Authorization: Bearer $MEILI_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"description": "Public search for products", "actions": ["search"], "indexes": ["products"], "expiresAt": null}'

The key field in the response is the value to use in your frontend. Test that it can search through Nginx:

curl -s -X POST https://search.your_domain/indexes/products/search \
  -H 'Authorization: Bearer your_search_key' \
  -H 'Content-Type: application/json' \
  --data '{"q": "hub"}'

and that it cannot write:

curl -s -X DELETE https://search.your_domain/indexes/products \
  -H 'Authorization: Bearer your_search_key'
{"message":"The provided API key is invalid.","code":"invalid_api_key","type":"auth","link":"https://docs.meilisearch.com/errors#invalid_api_key"}

For your backend, use the default admin key (or a dedicated one) instead of the master key. List existing keys with GET /keys.

Step 9 - Backing up with dumps

A dump is a portable export of all indexes, documents, settings and keys that can be imported into the same or a newer Meilisearch version. Create one:

curl -s -X POST http://127.0.0.1:7700/dumps -H "Authorization: Bearer $MEILI_MASTER_KEY"

When the task finishes, the .dump file appears in the dump directory:

sudo ls -lh /var/lib/meilisearch/dumps

Copy dumps off the server regularly. Dumps are also the supported way to upgrade between Meilisearch versions: create a dump, replace the binary, and start the new version once with --import-dump /var/lib/meilisearch/dumps/<file>.dump on an empty db_path.

Troubleshooting

Service fails with a master key error: in production mode the key must be at least 16 bytes. Check sudo journalctl -u meilisearch -n 30 and regenerate the key as in Step 3.

413 Request Entity Too Large when adding documents: the batch is larger than client_max_body_size in Nginx. Raise it, or send documents in smaller batches.

Search fails with invalid_search_filter: the attribute is not in filterableAttributes, or the settings task has not finished. Check GET /tasks?indexUids=products.

Conclusion

Meilisearch now runs as a hardened systemd service, reachable only through Nginx over HTTPS, with a master key for administration and a scoped key for public search. Next, integrate it with an official SDK (JavaScript, PHP, Python and others) or the InstantSearch frontend libraries, keep your index in sync from your application whenever data changes, and schedule a daily dump with a systemd timer.