Can I self-host Algolia?

YES · ONE COMMAND— setup effort 1 of 4

YES — it's called Meilisearch. It takes one prompt, a 1024 MB VPS, and about 10 minutes. There is no like-for-like Algolia price to quote for this swap, so this page quotes none.

Why people pay for Algolia

Stated as the vendor would want it stated. A replacement you pick without knowing what the subscription actually buys is a replacement you abandon in a fortnight.

Algolia sells search that feels instant on someone else's infrastructure: typo tolerance, ranking rules, analytics and a dashboard your PM can open, billed on records and search operations so the invoice grows with success.

Algolia plans and list prices
PlanList priceWhat it buys
Buildthe plan this page prices againstfreeFree tier with usage caps; overage and higher tiers are usage-priced.
Growquote onlyUsage-based; no honest flat monthly number without a traffic assumption.

Vendor list prices in USD, read from the pricing page on 2026-08-07 · confidence: low

Replaced by Meilisearch

One project, named before the prompt, so you know what you are about to install.

A typo-tolerant search API you run yourself, without per-search billing.

A self-hosted search engine API with typo tolerance and a simple HTTP interface. You run the binary, hold the master key, create a search-scoped API key for browsers, and keep the indexes on disk you control. License at the pin is MIT AND BUSL-1.1 with EE features under BUSL.

The swap

You're paying

Algolia

no list price to quote

is replaced by

You'd run

Meilisearch

ONE COMMAND · ~10 min to running · 1024 MB RAM

Algolia Build · a metered rate, not a whole bill · no like-for-like list price on the vendor's page, so this page quotes none

Before you start

RAM floor
1024 MBfloor from upstream docs — not measured by us yet
Disk
10 GBthe app, its data, and room for one backup
Domain needed
yes, one A recorda hostname pointed at the box before you start — TLS needs it on the cloud path, and the local path needs none
Time budget
~10 minunder 10 minutes, through the first backup

The prompt

Two paths to the same Meilisearch: the cloud one assumes Prompt Zero is done on a server you rent, the local one assumes nothing but a computer that can run Docker Desktop. Read whichever you pick before you paste it, which is the whole reason both are on the page instead of behind a download.

authored from upstream docs · not yet machine-verified · Claude Code

Where it runs

285 lines · 12,431 bytes

What this prompt will do
  1. Preflight
  2. Layout
  3. Secrets
  4. compose.yml
  5. Caddy and TLS
  6. Firewall
  7. Start and verify
  8. First backup and restore
  9. Updating later
  10. What will probably go wrong
  11. Out of scope

Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.

paste it into Claude Code in a terminal on your own machine · it runs the install over ssh vps

You are Claude Code on the user's machine. The user has completed Prompt Zero: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny.

Run every command in this prompt on the server over `ssh vps` unless the step says otherwise.

Install Meilisearch 1.52.0 on that server, reachable at https://<DOMAIN>, behind the existing
Caddy with automatic TLS.

## 1. Preflight

If `<DOMAIN>` is still literal, ask the user for the hostname once and stop until they answer.
Its A record must already point at this server.

Say two things first. One: this is a search API, not a website with accounts. There is no
browser sign-in to finish and no admin dashboard to claim. Two: without a master key the
instance is not gated the way a public hostname requires, so step 3 generates that key before
the container starts.

Meilisearch needs 1024 MB of RAM available and 10 GB free on /srv. The image publishes amd64
and arm64. Measure all four:

```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```

If available RAM is under 1024 MB or free disk is under 10 GB, print both numbers and stop. Do
not install and hope. If `dig +short` prints nothing, print that and stop: Caddy cannot certify
a hostname that does not resolve. The 1 GB floor is for a small personal index; multi-million
document catalogues need more RAM.

## 2. Layout

```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/meilisearch /srv/meilisearch/backups /srv/meilisearch/data
ls -la /srv/meilisearch
```

Assert: the directories exist and are owned by the login user. Nothing is written outside
/srv/meilisearch. Index files will appear under `data/` after the first documents are added.

## 3. Secrets

One secret: the master key. Generate it on the server. Do not print it, do not repeat it in
your summary, and do not put it in any log line. Upstream requires at least 16 bytes of valid
UTF-8; hex 32 is more than enough.

```bash
umask 077
cat > /srv/meilisearch/.env <<EOF
MEILI_MASTER_KEY=$(openssl rand -hex 32)
EOF
chmod 600 /srv/meilisearch/.env
umask 022
ls -la /srv/meilisearch/.env
```

Assert: `.env` is mode 600. Print only the path, never the key. Tell the user the key lives in
/srv/meilisearch/.env and they can read it later with
`sudo grep MEILI_MASTER_KEY /srv/meilisearch/.env`. It belongs in their password manager if
they will hand it to an application later. Never put the master key in a browser, a public
frontend, or a chat transcript. Step 7 creates a search-scoped key for client use.

## 4. compose.yml

```bash
cat > /srv/meilisearch/compose.yml <<'EOF'
# Meilisearch · the deterministic fallback. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
#   docker .............. https://www.meilisearch.com/docs/guides/misc/docker
#   security ............ https://www.meilisearch.com/docs/learn/security/basic_security
#   configuration ....... https://www.meilisearch.com/docs/resources/self_hosting/configuration/reference
#   license ............. https://github.com/meilisearch/meilisearch/blob/v1.52.0/LICENSE
#
# One container. MEILI_MASTER_KEY comes from env_file (generated on the server).
# MEILI_NO_ANALYTICS opts out of telemetry. Indexes live under /meili_data.
# Tag and digest are the v1.52.0 release read from Docker Hub on 2026-08-07.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  meilisearch:
    image: getmeili/meilisearch:v1.52.0@sha256:d36e713e8f89483af1ab0d72011bbd503f5ab100b68ccbfad51c39e3f0a0567d
    container_name: meilisearch
    restart: unless-stopped
    env_file: /srv/meilisearch/.env
    environment:
      MEILI_ENV: production
      MEILI_NO_ANALYTICS: "true"
    volumes:
      - /srv/meilisearch/data:/meili_data
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8203.
      - "127.0.0.1:8203:7700"
EOF
cd /srv/meilisearch && docker compose config >/dev/null && echo "compose OK"
```

Assert: that prints `compose OK`. One service, one published port, no database container:
indexes live under `data/`. `MEILI_ENV=production` and `MEILI_NO_ANALYTICS=true` are set in
the file; the master key stays only in `.env`. Do not add a Caddy service to this file.

## 5. Caddy and TLS

Append the block below to the Caddyfile Prompt Zero installed, with `<DOMAIN>` replaced by the
real hostname. Copy the file first: a syntax error here takes down every other site on the box.

```bash
cat > /srv/meilisearch/Caddyfile <<'EOF'
# Meilisearch · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://www.meilisearch.com/docs/learn/security/basic_security and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed, with
# <DOMAIN> replaced by the hostname pointed at this box. Caddy runs under systemd
# on the host. There is no Caddy container anywhere in this project. Auth is the
# master key inside Meilisearch, not a browser login form.

<DOMAIN> {
	encode zstd gzip

	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		X-Frame-Options "SAMEORIGIN"
		Referrer-Policy "no-referrer"
		-Server
	}

	# 8203 is the loopback port compose publishes; it is never in the firewall.
	reverse_proxy 127.0.0.1:8203
}
EOF
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-meilisearch
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sed "s|<DOMAIN>|${REAL_DOMAIN}|g" /srv/meilisearch/Caddyfile | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```

Set `REAL_DOMAIN` to the hostname the user gave in step 1 before running sed. Do not wrap the
value in extra quotes inside the sed replacement. Assert: validate and reload exit 0. If
validate fails, restore `/etc/caddy/Caddyfile.before-meilisearch`, reload, and report what it
objected to. Caddy requests the certificate on the first request and renews it on its own.

## 6. Firewall

Two ports open, both Caddy's. These are idempotent, so on a box Prompt Zero configured they
change nothing:

```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```

80/tcp answers the ACME challenge and redirects to HTTPS, 443/tcp is the only way in, and
443/udp is HTTP/3. 8203 stays closed because compose binds it to 127.0.0.1. Assert:
`ufw status verbose` prints `Status: active`, shows 80, 443/tcp and 443/udp, and no rule
mentioning 8203 or 7700.

## 7. Start and verify

```bash
cd /srv/meilisearch
docker compose pull
docker compose up -d
for i in $(seq 1 24); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:8203/health); echo "$i $code"; [ "$code" = 200 ] && break; sleep 5; done
curl -sS http://127.0.0.1:8203/health; echo
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/indexes
MASTER=$(grep MEILI_MASTER_KEY /srv/meilisearch/.env | cut -d= -f2-)
curl -sS -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer ${MASTER}" https://<DOMAIN>/indexes
curl -sS -H "Authorization: Bearer ${MASTER}" https://<DOMAIN>/keys
```

Assert all five, and print the HTTP codes (not the master key). The health loop ends on `200`
and the body is a health payload. The unauthenticated call to `/indexes` prints `401`: that is
the security assert in this block. The authenticated `/indexes` call prints `200`. The `/keys`
call returns JSON that includes default API keys, among them a search key whose actions include
`search`. If any assert misses, stop, run `docker compose logs --tail 40 meilisearch`, and name
the likely earlier step: a container that exits on a missing master key is step 3, and a 502
from Caddy with a running container is step 5. Unset `MASTER` when you are done reading keys:
`unset MASTER`. A running container is not success.

There is no sign-in page. Opening https://<DOMAIN>/ in a browser shows the API root JSON, not
an account form. The product is HTTP with `Authorization: Bearer …` headers.

STOP: tell the user to read the master key with
`sudo grep MEILI_MASTER_KEY /srv/meilisearch/.env`, store it offline, and create a
search-scoped key for any browser or public client by calling `GET /keys` (or creating one with
`POST /keys`) using only the master key on the server. Do not continue until they confirm they
have the master key stored and understand the search key is what frontends receive. Never ship
the master key to a browser.

A minimal handoff once they confirm, still on the server, without printing secrets into chat:

```bash
MASTER=$(grep MEILI_MASTER_KEY /srv/meilisearch/.env | cut -d= -f2-)
curl -sS -X POST https://<DOMAIN>/indexes \
  -H "Authorization: Bearer ${MASTER}" \
  -H 'Content-Type: application/json' \
  --data-binary '{"uid":"movies","primaryKey":"id"}'
curl -sS -X POST 'https://<DOMAIN>/indexes/movies/documents' \
  -H "Authorization: Bearer ${MASTER}" \
  -H 'Content-Type: application/json' \
  --data-binary '[{"id":1,"title":"Carol"},{"id":2,"title":"Wonder Woman"}]'
unset MASTER
```

Assert: both calls return task JSON (or index metadata) without a 401. Documents index
asynchronously; a search a few seconds later with a search API key should find `Carol`.

## 8. First backup and restore

One archive: the index data, the master key in `.env`, compose, and the live Caddy site block.

```bash
cd /srv/meilisearch
docker compose stop
sudo tar -czf /srv/meilisearch/backups/meilisearch-$(date +%F).tar.gz -C /srv/meilisearch data .env compose.yml -C /etc/caddy Caddyfile
docker compose start
ls -lh /srv/meilisearch/backups/
```

Assert: the archive exists and is non-empty. Print its size. Downtime is about five seconds;
the container is stopped so index files are not copied mid-write. Treat the archive as secret
material: it holds the master key.

A backup on the same disk as the data is not a backup. Run this from the user's machine, not
the server:

```bash
mkdir -p ~/backups/meilisearch
scp vps:/srv/meilisearch/backups/*.tar.gz ~/backups/meilisearch/
```

To restore: `docker compose down`, `sudo rm -rf /srv/meilisearch/data`, recreate `data` as in
step 2, untar the archive back into /srv/meilisearch (restores `data/`, `.env`, compose.yml),
put the Caddy block back if that is what was lost, then `docker compose up -d`. Tell the user
the honest split: `data/` is every document they indexed, `.env` is the master key that gates
the API, and restoring data without the key leaves a locked instance. Losing the key without a
copy means minting a new master key and accepting that old keys no longer match that instance
policy.

## 9. Updating later

New versions are listed at https://github.com/meilisearch/meilisearch/releases. The release tag
and the image tag are the same string, so release `v1.53.0` is image tag `v1.53.0`. Take a
backup first, then edit the image line in /srv/meilisearch/compose.yml to the new tag and its
digest:

```bash
cd /srv/meilisearch
docker compose pull
docker compose up -d
docker compose logs --tail 30 meilisearch
```

Watch that log until it settles, then re-run step 7's health and unauthenticated 401 checks
before calling the update done.

## 10. What will probably go wrong

You will paste the master key into a frontend "API key" field because it is the only string in
`.env` and it works in curl. I did that once on a demo page. Every visitor then held a key that
could delete indexes. Upstream is explicit: use the master key only to manage keys, and hand
browsers a search-scoped key from `/keys`. If you already leaked the master key, rotate by
stopping the container, generating a new `MEILI_MASTER_KEY` in `.env`, starting again, and
re-issuing every application key. Indexes survive; the old master key does not.

## 11. Out of scope

- Do not add a Caddy container to the compose file. Caddy is already running under systemd on
  this box, and a second one would fight it for 80 and 443.
- Do not publish 7700 or 8203 on the public interface or open them in the firewall. Caddy is
  the only way in.
- Do not leave `MEILI_MASTER_KEY` empty and do not run without `MEILI_ENV=production` on a
  public hostname.
- Do not enable Enterprise Edition features that require a commercial agreement. This install
  is the MIT-covered search API path of the dual-licensed tree.
No terminal agent? Use the chat fallback — slower, you paste the commands

For ChatGPT or Claude in a browser. The model cannot touch your server, so it hands you one command at a time and you run each one. Same install, more of your evening.

This path is slower: you paste every command yourself, and there is nobody watching the output
but you. If you can run Claude Code, use the other tab.

You are installing Meilisearch 1.52.0 on a VPS where Prompt Zero is done: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny. Run everything over `ssh vps`
unless a step says otherwise, and replace `<DOMAIN>` with the hostname whose A record already
points at the box.

Read these before step 1. This is a search API, not a website with accounts: there is no browser
sign-in to finish and no admin dashboard to claim. Without a master key the instance is not gated
the way a public hostname requires, so you will generate that key before the container starts.
Never put the master key in a browser; frontends get a search-scoped key from /keys. The tree at
the pinned tag is dual-licensed MIT AND BUSL-1.1: this install uses the MIT search API path, not
Enterprise Edition production features that need a commercial agreement.

## 1. Preflight

Its A record must already point at this server.

Meilisearch needs 1024 MB of RAM available and 10 GB free on /srv. The image publishes amd64
and arm64. Measure all four:

```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```

If available RAM is under 1024 MB or free disk is under 10 GB, print both numbers and stop. Do
not install and hope. If `dig +short` prints nothing, print that and stop: Caddy cannot certify
a hostname that does not resolve. The 1 GB floor is for a small personal index; multi-million
document catalogues need more RAM. A search engine without an application in front of it is an
empty box: this install starts the engine, and you still write the code that posts documents
and runs queries.


## 2. Layout

```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/meilisearch /srv/meilisearch/backups /srv/meilisearch/data
ls -la /srv/meilisearch
```

You should see `backups` and `data` owned by your login user. Nothing is written outside
/srv/meilisearch. Index files appear under `data/` after the first documents are added. Until
then the directory stays almost empty, which is normal.


## 3. Secrets

One secret: the master key. Generate it on the server. Do not paste the value into chat or into a public page. Upstream requires at least 16 bytes of valid
UTF-8; hex 32 is more than enough.

```bash
umask 077
cat > /srv/meilisearch/.env <<EOF
MEILI_MASTER_KEY=$(openssl rand -hex 32)
EOF
chmod 600 /srv/meilisearch/.env
umask 022
ls -la /srv/meilisearch/.env
```

Assert: `.env` is mode 600. Print only the path, never the key. Read it later with
`sudo grep MEILI_MASTER_KEY /srv/meilisearch/.env` and store it offline. Never put the master
key in a browser or public frontend. Step 7 creates a search-scoped key for client use.

## 4. compose.yml

```bash
cat > /srv/meilisearch/compose.yml <<'EOF'
# Meilisearch · the deterministic fallback. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
#   docker .............. https://www.meilisearch.com/docs/guides/misc/docker
#   security ............ https://www.meilisearch.com/docs/learn/security/basic_security
#   configuration ....... https://www.meilisearch.com/docs/resources/self_hosting/configuration/reference
#   license ............. https://github.com/meilisearch/meilisearch/blob/v1.52.0/LICENSE
#
# One container. MEILI_MASTER_KEY comes from env_file (generated on the server).
# MEILI_NO_ANALYTICS opts out of telemetry. Indexes live under /meili_data.
# Tag and digest are the v1.52.0 release read from Docker Hub on 2026-08-07.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  meilisearch:
    image: getmeili/meilisearch:v1.52.0@sha256:d36e713e8f89483af1ab0d72011bbd503f5ab100b68ccbfad51c39e3f0a0567d
    container_name: meilisearch
    restart: unless-stopped
    env_file: /srv/meilisearch/.env
    environment:
      MEILI_ENV: production
      MEILI_NO_ANALYTICS: "true"
    volumes:
      - /srv/meilisearch/data:/meili_data
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8203.
      - "127.0.0.1:8203:7700"
EOF
cd /srv/meilisearch && docker compose config >/dev/null && echo "compose OK"
```

You should see `compose OK`. One service, one published port, no database container: indexes
live under `data/`. `MEILI_ENV=production` and `MEILI_NO_ANALYTICS=true` are set in the file;
the master key stays only in `.env`. Do not add a Caddy service to this file. Telemetry is off
on purpose so anonymous usage stats do not leave this box.


## 5. Caddy and TLS

Append the block below to the Caddyfile Prompt Zero installed, with `<DOMAIN>` replaced by the
real hostname. Copy the file first: a syntax error here takes down every other site on the box.

```bash
cat > /srv/meilisearch/Caddyfile <<'EOF'
# Meilisearch · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://www.meilisearch.com/docs/learn/security/basic_security and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed, with
# <DOMAIN> replaced by the hostname pointed at this box. Caddy runs under systemd
# on the host. There is no Caddy container anywhere in this project. Auth is the
# master key inside Meilisearch, not a browser login form.

<DOMAIN> {
	encode zstd gzip

	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		X-Frame-Options "SAMEORIGIN"
		Referrer-Policy "no-referrer"
		-Server
	}

	# 8203 is the loopback port compose publishes; it is never in the firewall.
	reverse_proxy 127.0.0.1:8203
}
EOF
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-meilisearch
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sed "s|<DOMAIN>|${REAL_DOMAIN}|g" /srv/meilisearch/Caddyfile | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```

Set `REAL_DOMAIN` to your real hostname before running sed (example:
`REAL_DOMAIN=search.example.com`). Do not wrap the value in extra quotes inside the sed
replacement. Validate and reload should exit 0. If validate fails, restore
`/etc/caddy/Caddyfile.before-meilisearch`, reload, and read the error. Caddy requests the
certificate on the first request and renews it on its own.

## 6. Firewall

Two ports open, both Caddy's. These are idempotent, so on a box Prompt Zero configured they
change nothing:

```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```

80/tcp answers the ACME challenge and redirects to HTTPS, 443/tcp is the only way in, and
443/udp is HTTP/3. 8203 stays closed because compose binds it to 127.0.0.1. You should see
`Status: active`, rules for 80 and 443, and no rule mentioning 8203 or 7700. Opening 7700 would
put the API on the public internet without Caddy's TLS path.


## 7. Start and verify

```bash
cd /srv/meilisearch
docker compose pull
docker compose up -d
for i in $(seq 1 24); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:8203/health); echo "$i $code"; [ "$code" = 200 ] && break; sleep 5; done
curl -sS http://127.0.0.1:8203/health; echo
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/indexes
MASTER=$(grep MEILI_MASTER_KEY /srv/meilisearch/.env | cut -d= -f2-)
curl -sS -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer ${MASTER}" https://<DOMAIN>/indexes
curl -sS -H "Authorization: Bearer ${MASTER}" https://<DOMAIN>/keys
```

You should see: health loop ends on `200`; unauthenticated `/indexes` is `401` (the security
assert); authenticated `/indexes` is `200`; `/keys` lists default keys including a search key.
Print HTTP codes, not the master key. If any miss, run `docker compose logs --tail 40
meilisearch`. Unset `MASTER` when done: `unset MASTER`.

There is no sign-in page. Opening https://<DOMAIN>/ shows API JSON, not an account form.

STOP: read the master key with `sudo grep MEILI_MASTER_KEY /srv/meilisearch/.env`, store it
offline, and plan to use only a search-scoped key from `/keys` in any browser. Do not continue until they confirm they have the master key stored and understand the search key is what frontends receive.

Sample handoff once stored (do not paste secret values into chat):

```bash
MASTER=$(grep MEILI_MASTER_KEY /srv/meilisearch/.env | cut -d= -f2-)
curl -sS -X POST https://<DOMAIN>/indexes \
  -H "Authorization: Bearer ${MASTER}" \
  -H 'Content-Type: application/json' \
  --data-binary '{"uid":"movies","primaryKey":"id"}'
curl -sS -X POST 'https://<DOMAIN>/indexes/movies/documents' \
  -H "Authorization: Bearer ${MASTER}" \
  -H 'Content-Type: application/json' \
  --data-binary '[{"id":1,"title":"Carol"},{"id":2,"title":"Wonder Woman"}]'
unset MASTER
```

Both calls should return task JSON without a 401. Documents index asynchronously; a search a
few seconds later with a search API key should find `Carol`.

## 8. First backup and restore

One archive: the index data, the master key in `.env`, compose, and the live Caddy site block.

```bash
cd /srv/meilisearch
docker compose stop
sudo tar -czf /srv/meilisearch/backups/meilisearch-$(date +%F).tar.gz -C /srv/meilisearch data .env compose.yml -C /etc/caddy Caddyfile
docker compose start
ls -lh /srv/meilisearch/backups/
```

The archive should exist and be non-empty. Print its size. Downtime is about five seconds.
Treat the archive as secret material: it holds the master key.

A backup on the same disk is not a backup. From your own machine:

```bash
mkdir -p ~/backups/meilisearch
scp vps:/srv/meilisearch/backups/*.tar.gz ~/backups/meilisearch/
```

To restore: `docker compose down`, remove `data`, recreate it, untar into /srv/meilisearch,
put the Caddy block back if needed, then `docker compose up -d`. `data/` is every document you
indexed; `.env` is the master key. They travel together or the API stays locked.



After the first real application is wired, run one deliberate search and one deliberate delete
of a test index so you know which key can do which. A search key that unexpectedly returns 403
on a write is correct behaviour. An admin key in a browser is the failure mode step 10 describes.

On restore after disk loss: bring Docker and Caddy back (Prompt Zero), restore the tar into
/srv/meilisearch, restore the Caddyfile, `docker compose up -d`, then prove unauthenticated
calls still return 401 and authenticated /health or /indexes still return 200 with the restored
master key. If you only restore `data/` and regenerate `.env`, the instance starts but old
application keys and assumptions about the previous master key no longer hold.

## 9. Updating later

New versions are listed at https://github.com/meilisearch/meilisearch/releases. The release tag
and the image tag are the same string, so release `v1.53.0` is image tag `v1.53.0`. Take a
backup first, then edit the image line in /srv/meilisearch/compose.yml to the new tag and its
digest:

```bash
cd /srv/meilisearch
docker compose pull
docker compose up -d
docker compose logs --tail 30 meilisearch
```

Watch that log until it settles, then re-run step 7's health and unauthenticated 401 checks
before calling the update done. If a release notes page mentions a one-way dump or reindex,
read it before pulling: index formats can force a rebuild after major jumps.


## 10. What will probably go wrong

You will paste the master key into a frontend "API key" field because it is the only string in
`.env` and it works in curl. I did that once on a demo page. Every visitor then held a key that
could delete indexes. Upstream is explicit: use the master key only to manage keys, and hand
browsers a search-scoped key from `/keys`. If you already leaked the master key, rotate by
stopping the container, generating a new `MEILI_MASTER_KEY` in `.env`, starting again, and
re-issuing every application key. Indexes survive; the old master key does not.

The other miss is disk. A bulk import of a large crawl without checking free space fills
`/srv/meilisearch/data` until the container cannot write. Watch `df -h /srv` during the first
real index job, and keep a second disk plan for anything that grows like a product catalogue.

## 11. Out of scope

- Do not add a Caddy container to the compose file. Caddy is already running under systemd on
  this box, and a second one would fight it for 80 and 443.
- Do not publish 7700 or 8203 on the public interface or open them in the firewall. Caddy is
  the only way in.
- Do not leave `MEILI_MASTER_KEY` empty and do not run without `MEILI_ENV=production` on a
  public hostname.
- Do not skip the first backup after indexes hold real data.
- Do not enable Enterprise Edition features that require a commercial agreement. This install
  is the MIT-covered search API path of the dual-licensed tree.

SPDX at the pin is MIT AND BUSL-1.1 in one repository. Community Edition search is MIT.
Enterprise Edition paths (sharding, S3-streaming snapshots and related EE work) are BUSL-1.1
and are not free to run in production without a commercial agreement with Meilisearch. Source-
available candor applies to the BUSL part: it is not OSI open source for those paths.

When you integrate with an app, prefer the default search API key listed by GET /keys for any
code that runs in a browser, and keep the default admin API key on the server the same way you
keep the master key. Upstream warns not to expose admin keys on a public frontend.

Keep one off-box copy of the backup whenever indexes hold data you cannot re-crawl cheaply.

242 lines · 10,108 bytes

What this prompt will do
  1. Preflight
  2. Docker
  3. Layout
  4. Secrets
  5. compose.yml
  6. Firewall
  7. Start and verify
  8. First backup and restore
  9. Updating later
  10. What will probably go wrong
  11. Out of scope

Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.

paste it into Claude Code in a terminal on this computer · installs Docker Desktop if it is missing · no server, no domain

You are Claude Code on the user's own computer. There is no server and no Prompt Zero:
everything in this prompt runs on this machine and stays on it.

Run every command on this computer, in the shell you are already in. Nothing in this prompt
uses ssh.

Install Meilisearch 1.52.0 under ~/selfhost/meilisearch, answering at http://localhost:8203.

## 1. Preflight

Say this to the user before anything installs. This is a search API on this computer only:
http://localhost:8203 is unreachable from a phone or another laptop. What they get is an
engine their local app can index and query while they develop, not a shared production API.
There is no browser sign-in to finish.

Detect the OS and measure the machine:

```bash
uname -s
case "$(uname -s)" in
  Darwin) vm_stat | awk '/page size/{p=$8} /free|inactive/{s+=$3} END {printf "%d MB available\n", s*p/1048576}' ;;
  Linux) . /etc/os-release && echo "$ID $VERSION_CODENAME"; free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}' ;;
  MINGW*|MSYS*) powershell -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory" | awk '$1+0 {printf "%d MB available\n", $1/1024}' ;;
esac
df -h ~
```

`Darwin` is macOS, `Linux` is Linux, `MINGW` or `MSYS` is Windows under Git Bash. On Linux the
distribution ID and codename print next, for step 2. Meilisearch needs 1024 MB of RAM available
and 10 GB free on the home disk, and the image publishes amd64 and arm64. Every branch prints
free memory; on macOS and Windows Docker Desktop takes its allocation out of the host figure.
If available RAM is under 1024 MB or free disk is under 10 GB, print both numbers and stop.

## 2. Docker

Check before installing anything:

```bash
docker info >/dev/null 2>&1 && echo "docker OK" || echo "docker MISSING"
docker compose version 2>/dev/null || true
```

If that printed `docker OK` and a compose version, skip to step 3.

Otherwise, install Docker for the OS step 1 detected:

- macOS: if `command -v brew` succeeds, run `brew install --cask docker`. If there is no
  Homebrew, STOP: tell the user to download Docker Desktop from
  https://www.docker.com/products/docker-desktop/ and install it, and wait until they
  confirm. Either way, then STOP: tell the user to open Docker Desktop once, accept its
  terms, and wait for the whale icon to say it is running. Do not continue until they
  confirm.
- Windows: run `winget install -e --id Docker.DockerDesktop`. If winget is missing or the
  install fails, STOP: tell the user to download Docker Desktop from the URL above and
  install it, and wait until they confirm. Docker Desktop configures WSL 2 itself and may
  ask for a reboot; if it does, STOP and tell the user to reboot and come back, this
  prompt resumes at this step. Then STOP: have the user open Docker Desktop, accept its
  terms, and confirm it says running.
- Linux, Debian or Ubuntu: install Docker Engine from download.docker.com's apt
  repository, with its signing key saved to a file first, never piped into a shell. The
  fence is guarded, a no-op on anything but a Linux with apt:

```bash
if [ "$(uname -s)" = "Linux" ] && command -v apt-get >/dev/null 2>&1; then
  sudo apt-get update
  sudo apt-get install -y ca-certificates curl
  sudo install -m 0755 -d /etc/apt/keyrings
  sudo curl -fsSL https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/gpg -o /etc/apt/keyrings/docker.asc
  sudo chmod a+r /etc/apt/keyrings/docker.asc
  echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$(. /etc/os-release && echo "$ID") $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
  sudo apt-get update
  sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
  sudo usermod -aG docker "$USER"
fi
```

  Adding the user to the docker group is root-equivalent on this machine; say that to the
  user in one sentence, and tell them the group change lands at their next login.
- Linux, anything else: STOP. Tell the user to install Docker Engine and the compose
  plugin with their distribution's package manager, and to run this prompt again once
  `docker info` works.

Assert: `docker info` exits 0 and `docker compose version` prints a version. Do not
continue without both.

## 3. Layout

```bash
mkdir -p ~/selfhost/meilisearch/data ~/selfhost/meilisearch/backups
ls -la ~/selfhost/meilisearch
```

Assert: `data` and `backups` exist. Index files land under `data/` after documents are added.

## 4. Secrets

One secret: the master key. Generate it here. Do not print it into chat.

```bash
umask 077
cat > ~/selfhost/meilisearch/.env <<EOF
MEILI_MASTER_KEY=$(openssl rand -hex 32)
EOF
chmod 600 ~/selfhost/meilisearch/.env
umask 022
ls -l ~/selfhost/meilisearch/.env
```

Assert: mode `-rw-------` (advisory on Windows NTFS). Tell the user to read it with
`grep MEILI_MASTER_KEY ~/selfhost/meilisearch/.env` when they wire an app. Never put the
master key in a browser; use a search-scoped key from `/keys` for frontends.

## 5. compose.yml

```bash
cat > ~/selfhost/meilisearch/compose.yml <<'EOF'
# Meilisearch · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
#   docker .............. https://www.meilisearch.com/docs/guides/misc/docker
#   security ............ https://www.meilisearch.com/docs/learn/security/basic_security
#   configuration ....... https://www.meilisearch.com/docs/resources/self_hosting/configuration/reference
#   license ............. https://github.com/meilisearch/meilisearch/blob/v1.52.0/LICENSE
#
# One container on the computer you are sitting at. Paths are relative to
# ~/selfhost/meilisearch/. MEILI_MASTER_KEY comes from ./.env. Tag and digest
# are the v1.52.0 release read from Docker Hub on 2026-08-07.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  meilisearch:
    image: getmeili/meilisearch:v1.52.0@sha256:d36e713e8f89483af1ab0d72011bbd503f5ab100b68ccbfad51c39e3f0a0567d
    container_name: meilisearch
    restart: unless-stopped
    env_file: ./.env
    environment:
      MEILI_ENV: production
      MEILI_NO_ANALYTICS: "true"
    volumes:
      - ./data:/meili_data
    ports:
      # Loopback only: no other device on the wifi can reach 8203.
      - "127.0.0.1:8203:7700"
EOF
cd ~/selfhost/meilisearch && docker compose config >/dev/null && echo "compose OK"
```

Assert: that prints `compose OK`.

## 6. Firewall

Nothing to open. Confirm loopback binding:

```bash
grep -c '"127.0.0.1:' ~/selfhost/meilisearch/compose.yml
```

Assert: that prints `1`. Do not rebind to `0.0.0.0`.

## 7. Start and verify

```bash
cd ~/selfhost/meilisearch
docker compose pull
docker compose up -d
for i in $(seq 1 24); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://localhost:8203/health); echo "$i $code"; [ "$code" = 200 ] && break; sleep 5; done
curl -sS http://localhost:8203/health; echo
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8203/indexes
MASTER=$(grep MEILI_MASTER_KEY ~/selfhost/meilisearch/.env | cut -d= -f2-)
curl -sS -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer ${MASTER}" http://localhost:8203/indexes
curl -sS -H "Authorization: Bearer ${MASTER}" http://localhost:8203/keys | head -c 400; echo
unset MASTER
```

Assert: health is 200; unauthenticated `/indexes` is `401`; authenticated `/indexes` is `200`;
`/keys` returns JSON listing default keys. Print the codes, not the master key. If the 401 is
missing, the master key did not load: check `.env` and recreate it from step 4.

STOP: tell the user there is no sign-in UI, that apps talk HTTP with Bearer tokens, and that
they should store the master key offline and create a search key via `/keys` for any browser
code. Do not continue until they confirm.

Sample index handoff:

```bash
MASTER=$(grep MEILI_MASTER_KEY ~/selfhost/meilisearch/.env | cut -d= -f2-)
curl -sS -X POST http://localhost:8203/indexes \
  -H "Authorization: Bearer ${MASTER}" \
  -H 'Content-Type: application/json' \
  --data-binary '{"uid":"movies","primaryKey":"id"}'
curl -sS -X POST 'http://localhost:8203/indexes/movies/documents' \
  -H "Authorization: Bearer ${MASTER}" \
  -H 'Content-Type: application/json' \
  --data-binary '[{"id":1,"title":"Carol"},{"id":2,"title":"Wonder Woman"}]'
unset MASTER
```

## 8. First backup and restore

```bash
cd ~/selfhost/meilisearch
docker compose stop
tar -C ~/selfhost/meilisearch -czf ~/selfhost/meilisearch/backups/meilisearch-$(date +%F).tar.gz data .env compose.yml
docker compose start
ls -lh ~/selfhost/meilisearch/backups/
```

Assert: the archive exists and is non-empty. Print its size. It holds the master key; treat it
as secret. Ask the user for a destination that leaves this computer and copy it with `cp`.

To restore: `docker compose down`, remove `data`, untar into ~/selfhost/meilisearch, then
`docker compose up -d`. `data/` is the indexes; `.env` is the master key; they travel together.

## 9. Updating later

New versions are at https://github.com/meilisearch/meilisearch/releases. Take a backup first,
then edit the image line in ~/selfhost/meilisearch/compose.yml to the new tag and digest:

```bash
cd ~/selfhost/meilisearch
docker compose pull
docker compose up -d
docker compose logs --tail 30 meilisearch
```

Re-run step 7's health and 401 checks before calling the update done.

## 10. What will probably go wrong

You will wire the master key into a React demo because it is the only credential you have and
search starts working. Anyone who opens the page then holds a key that can wipe indexes.
Create a search-scoped key from `/keys` for the browser and keep the master key on the server
side only. If you already pasted it into client code, rotate the master key in `.env` and
restart.

## 11. Out of scope

- Do not expose this to the internet.
- Do not configure port forwarding on the router.
- Do not add a reverse proxy or TLS.
- Do not rebind 8203 to 0.0.0.0.
- Do not leave `MEILI_MASTER_KEY` empty.
compose.local.ymlthe services, pinned · local layout27 lines

authored from upstream docs, never pasted · 1,254 bytes

# Meilisearch · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
#   docker .............. https://www.meilisearch.com/docs/guides/misc/docker
#   security ............ https://www.meilisearch.com/docs/learn/security/basic_security
#   configuration ....... https://www.meilisearch.com/docs/resources/self_hosting/configuration/reference
#   license ............. https://github.com/meilisearch/meilisearch/blob/v1.52.0/LICENSE
#
# One container on the computer you are sitting at. Paths are relative to
# ~/selfhost/meilisearch/. MEILI_MASTER_KEY comes from ./.env. Tag and digest
# are the v1.52.0 release read from Docker Hub on 2026-08-07.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  meilisearch:
    image: getmeili/meilisearch:v1.52.0@sha256:d36e713e8f89483af1ab0d72011bbd503f5ab100b68ccbfad51c39e3f0a0567d
    container_name: meilisearch
    restart: unless-stopped
    env_file: ./.env
    environment:
      MEILI_ENV: production
      MEILI_NO_ANALYTICS: "true"
    volumes:
      - ./data:/meili_data
    ports:
      # Loopback only: no other device on the wifi can reach 8203.
      - "127.0.0.1:8203:7700"

agent-readable mirror: /self-host/algolia.md

The files, if you'd rather do it yourself

The cloud path with no agent involved: three files, in the order you'd use them. The cloud prompt above writes exactly these — if the two ever disagree, the files are the ones CI diffs. The local path ships its own compose file, collapsed under its own prompt.

compose.ymlthe services, pinned27 lines

authored from upstream docs, never pasted · 1,295 bytes

# Meilisearch · the deterministic fallback. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
#   docker .............. https://www.meilisearch.com/docs/guides/misc/docker
#   security ............ https://www.meilisearch.com/docs/learn/security/basic_security
#   configuration ....... https://www.meilisearch.com/docs/resources/self_hosting/configuration/reference
#   license ............. https://github.com/meilisearch/meilisearch/blob/v1.52.0/LICENSE
#
# One container. MEILI_MASTER_KEY comes from env_file (generated on the server).
# MEILI_NO_ANALYTICS opts out of telemetry. Indexes live under /meili_data.
# Tag and digest are the v1.52.0 release read from Docker Hub on 2026-08-07.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  meilisearch:
    image: getmeili/meilisearch:v1.52.0@sha256:d36e713e8f89483af1ab0d72011bbd503f5ab100b68ccbfad51c39e3f0a0567d
    container_name: meilisearch
    restart: unless-stopped
    env_file: /srv/meilisearch/.env
    environment:
      MEILI_ENV: production
      MEILI_NO_ANALYTICS: "true"
    volumes:
      - /srv/meilisearch/data:/meili_data
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8203.
      - "127.0.0.1:8203:7700"
Caddyfilethe hostname and TLS25 lines

authored from upstream docs, never pasted · 842 bytes

# Meilisearch · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://www.meilisearch.com/docs/learn/security/basic_security and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed, with
# <DOMAIN> replaced by the hostname pointed at this box. Caddy runs under systemd
# on the host. There is no Caddy container anywhere in this project. Auth is the
# master key inside Meilisearch, not a browser login form.

<DOMAIN> {
	encode zstd gzip

	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		X-Frame-Options "SAMEORIGIN"
		Referrer-Policy "no-referrer"
		-Server
	}

	# 8203 is the loopback port compose publishes; it is never in the firewall.
	reverse_proxy 127.0.0.1:8203
}
install.shthe same install, no agent131 lines

authored from upstream docs, never pasted · 5,601 bytes

#!/usr/bin/env bash
# Meilisearch · the agent-free install.
#
# Everything prompt.md tells an agent to do, as a script you can read first.
# Run it on the VPS, as a non-root user who is in the docker group:
#
#   DOMAIN_HOST=search.example.com ./install.sh
#
# Authored by caniselfhostit from the upstream documentation:
#   https://www.meilisearch.com/docs/guides/misc/docker
#   https://www.meilisearch.com/docs/learn/security/basic_security
#   https://www.meilisearch.com/docs/resources/self_hosting/configuration/reference
#   https://github.com/meilisearch/meilisearch/blob/v1.52.0/LICENSE
#
# One secret is generated here: MEILI_MASTER_KEY. It goes into
# /srv/meilisearch/.env with mode 600 and is never printed. There is no browser
# sign-in. Unauthenticated API calls must return 401 once the key is set.
#
# NOT YET VERIFIED: no harness run has been recorded against this script.
set -euo pipefail

APP_DIR="${APP_DIR:-/srv/meilisearch}"
DOMAIN_HOST="${DOMAIN_HOST:-}"

die() { printf 'install.sh: %s\n' "$1" >&2; exit 1; }

# --- 1. Refuse to start on a machine that is not ready -----------------------

[ -n "$DOMAIN_HOST" ] || die "set DOMAIN_HOST to the hostname you pointed at this server, e.g. search.example.com"
command -v docker >/dev/null 2>&1 || die "docker is not installed. Run Prompt Zero first."
docker compose version >/dev/null 2>&1 || die "the docker compose plugin is missing"
command -v caddy >/dev/null 2>&1 || die "caddy is not installed on the host. Run Prompt Zero first."
command -v openssl >/dev/null 2>&1 || die "openssl is not installed"

avail_mb="$(free -m | awk '/^Mem:/ {print $7}')"
[ "$avail_mb" -ge 1024 ] || die "only ${avail_mb} MB of RAM available; this install wants 1024 MB"
avail_gb="$(df -BG --output=avail /srv | tail -1 | tr -dc '0-9')"
[ "$avail_gb" -ge 10 ] || die "only ${avail_gb} GB free on /srv; this install wants 10 GB"

resolved="$(getent hosts "$DOMAIN_HOST" | awk '{print $1; exit}' || true)"
[ -n "$resolved" ] || die "$DOMAIN_HOST does not resolve yet. Add the A record, wait a minute, run this again."

# --- 2. Lay the files out ----------------------------------------------------

sudo install -d -m 750 -o "$(id -u)" -g "$(id -g)" \
	"$APP_DIR" "$APP_DIR/backups" "$APP_DIR/data"
install -m 0644 "$(dirname "$0")/compose.yml" "$APP_DIR/compose.yml"
install -m 0644 "$(dirname "$0")/Caddyfile" "$APP_DIR/Caddyfile"

# --- 3. Master key on the server ---------------------------------------------

if [ ! -f "$APP_DIR/.env" ]; then
	umask 077
	cat > "$APP_DIR/.env" <<-EOF
		MEILI_MASTER_KEY=$(openssl rand -hex 32)
	EOF
	chmod 600 "$APP_DIR/.env"
	umask 022
fi

cd "$APP_DIR"
docker compose config >/dev/null

# --- 4. Caddy site block -----------------------------------------------------

if ! sudo grep -qF "$DOMAIN_HOST {" /etc/caddy/Caddyfile; then
	sudo cp /etc/caddy/Caddyfile "/etc/caddy/Caddyfile.before-meilisearch"
	printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
	sed "s|<DOMAIN>|${DOMAIN_HOST}|g" "$APP_DIR/Caddyfile" | sudo tee -a /etc/caddy/Caddyfile >/dev/null
fi
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

# --- 5. Firewall -------------------------------------------------------------

if command -v ufw >/dev/null 2>&1; then
	echo "==> 80/tcp and 443/tcp for Caddy, 443/udp for HTTP/3; 8203 stays closed"
	sudo ufw allow 80/tcp
	sudo ufw allow 443/tcp
	sudo ufw allow 443/udp
	sudo ufw status verbose
fi

# --- 6. Start and assert -----------------------------------------------------

docker compose pull
docker compose up -d

echo "==> waiting for http://127.0.0.1:8203/health"
for _ in $(seq 1 24); do
	code="$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:8203/health" || true)"
	[ "$code" = "200" ] && break
	sleep 5
done
[ "${code:-}" = "200" ] || die "/health answered ${code:-nothing}. Check: docker compose logs --tail 40 meilisearch"

unauth="$(curl -sS -o /dev/null -w '%{http_code}' "https://${DOMAIN_HOST}/indexes" || true)"
[ "$unauth" = "401" ] || die "unauthenticated /indexes returned ${unauth}, not 401. Stop and investigate."

MASTER="$(grep MEILI_MASTER_KEY "$APP_DIR/.env" | cut -d= -f2-)"
auth="$(curl -sS -o /dev/null -w '%{http_code}' -H "Authorization: Bearer ${MASTER}" "https://${DOMAIN_HOST}/indexes" || true)"
[ "$auth" = "200" ] || die "authenticated /indexes returned ${auth}, not 200"
curl -sS -H "Authorization: Bearer ${MASTER}" "https://${DOMAIN_HOST}/keys" | grep -q 'Default Search API Key' \
	|| die "/keys did not list the default search key"
unset MASTER

# --- 7. First backup ---------------------------------------------------------

STAMP="$(date +%Y%m%d-%H%M%S)"
docker compose stop
sudo tar -czf "$APP_DIR/backups/meilisearch-${STAMP}.tar.gz" \
	-C "$APP_DIR" data .env compose.yml -C /etc/caddy Caddyfile
docker compose start
ls -lh "$APP_DIR/backups/"
[ -s "$APP_DIR/backups/meilisearch-${STAMP}.tar.gz" ] || die "the backup archive is empty"

cat <<-DONE

	Meilisearch is answering at https://${DOMAIN_HOST}

	  1. There is no browser sign-in. The API expects Authorization: Bearer keys.
	  2. Master key: sudo grep MEILI_MASTER_KEY ${APP_DIR}/.env
	     Store it offline. Never put it in a frontend.
	  3. List keys (search-scoped for browsers):
	       MASTER=\$(grep MEILI_MASTER_KEY ${APP_DIR}/.env | cut -d= -f2-)
	       curl -sS -H "Authorization: Bearer \${MASTER}" https://${DOMAIN_HOST}/keys
	  4. Unauthenticated /indexes returns 401 (asserted above).
	  5. First backup at ${APP_DIR}/backups (includes .env). Copy it off this disk.
	  6. NOT YET VERIFIED on a clean harness machine.

DONE

What you're signing up for

The part a vendor's comparison page leaves out. None of it is a reason not to do this; all of it is yours the moment you cancel Algolia.

  • Source-available, not open source for the whole tree: SPDX at the pinned tag is MIT AND BUSL-1.1. Community Edition search is MIT; Enterprise Edition paths (sharding, S3-streaming snapshots and related EE work) are BUSL-1.1 and are not free to run in production without a commercial agreement. This install runs the published image and the MIT-covered search API you index against; it does not unlock EE-only features.
  • This is a search engine API, not a hosted dashboard product. You still write the code that indexes documents and queries them. There is no browser sign-in to finish.
  • You own the master key. Anyone with it can read and write every index and mint other keys; it is generated on the server and never pasted into a browser. Search clients get a search-scoped key from /keys.
  • You own disk growth. Indexes are not free, and a large crawl without retention planning will fill the volume.
  • High availability, multi-region analytics dashboards and managed ranking labs are not in this container. Those are what Algolia sells.

Where this came from

“Only use the master key to manage API keys. Never use the master key to perform searches or other common operations.”

  • The official Docker image exposes port 7700 and stores indexes under a configurable data path (/meili_data by default). source
  • MEILI_MASTER_KEY must be set for a protected instance; without a master key, requests are not gated the way a public host requires. source
  • At v1.52.0 the repository SPDX is MIT AND BUSL-1.1: parts of the tree are MIT and Meilisearch Enterprise Edition paths are under BUSL-1.1, not a separate product only. source
  • MEILI_NO_ANALYTICS deactivates built-in telemetry when set. source
  • Caddy obtains and renews TLS certificates automatically for any public hostname named in the Caddyfile. source

Questions people actually ask

Answered from this page's own data — the same numbers, in sentences.

  • Can I self-host Algolia?

    Not Algolia itself — the vendor does not ship a version you can run on your own server. What you can self-host is the job people pay it for, and the answer to that is Meilisearch. A typo-tolerant search API you run yourself, without per-search billing. The install is one command: one container behind Caddy with automatic TLS, secrets generated on the server rather than in a chat window, and a first backup taken before the agent says it is done, in about 10 minutes. The prompt on this page does it; the compose.yml, Caddyfile and install.sh below do the same install with no agent at all.

  • What replaces Algolia?

    Meilisearch. A typo-tolerant search API you run yourself, without per-search billing. A self-hosted search engine API with typo tolerance and a simple HTTP interface. You run the binary, hold the master key, create a search-scoped API key for browsers, and keep the indexes on disk you control. License at the pin is MIT AND BUSL-1.1 with EE features under BUSL. Meilisearch is MIT AND BUSL-1.1-licensed and free; nothing on this page is a hosted service we sell you.

  • What does self-hosting cost compared to Algolia?

    1024 MB of RAM and 10 GB of disk — the smallest tier most VPS hosts sell, about $5 a month. Meilisearch itself is free and MIT AND BUSL-1.1-licensed; the bill is the server, plus a domain you probably already own. There is no like-for-like Algolia list price behind this swap, so this page does not invent a savings figure.

  • How hard is it really?

    ONE COMMAND — under 10 minutes. The rule that produced that verdict: one container, no database, no outside integration, at most one secret. Nothing to negotiate with anyone else, nothing to back up separately, at most one secret to generate. This is the case where the compose file honestly is the whole install. The tier is derived from seven countable facts about the Meilisearch install, not from anyone's impression of it, and the whole rubric is published on the methodology page.

  • Can I run Meilisearch on my own computer instead of a server?

    Yes — that is the second path in the prompt box above. "On my computer" installs the same Meilisearch on the machine you are sitting at: no VPS, no domain, no DNS, and nothing exposed to the internet. It checks for Docker first and installs Docker Desktop if the machine does not have it — macOS, Windows and Linux each get their own step — then binds everything to loopback, so the app answers on http://localhost and only on that computer. Worth knowing: Search only answers on this computer's localhost port, which is what you want for a personal app, and useless as a shared production API until you put it on a server. Same discipline as the cloud path: pinned images, secrets generated on the machine, and a first backup taken before the prompt says it is done.

Content last checked 2026-08-07. Verdicts are derived from the published rubric on /methodology; corrections go through the issue tracker.