Can I self-host Perplexity?

YES · ONE EVENING— setup effort 2 of 4

YES — it's called Vane. It takes one prompt, a 2048 MB VPS, and about 90 minutes. That is $20 a month you stop paying Perplexity — $240 a year on the Pro plan.

Why people pay for Perplexity

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.

Perplexity sells the two halves of an answer at once: a search index it crawls and maintains itself, and frontier models it has already paid for. You type a question and get a written answer with numbered sources under it, on a phone, in a browser extension, at a flat monthly price that does not move when you have a heavy week. The index is the part that is genuinely hard to replace, and the flat rate is the part people are really buying.

Perplexity plans and list prices
PlanList priceWhat it buys
FreefreeA daily allowance of the advanced search mode, then the basic one.
Prothe plan this page prices against$20/moAdvertised at $200 a year on the annual plan, which is the figure the marketing pages lead with.
Max$200/moAdvertised at $2,000 a year on the annual plan. Adds unlimited use of the deep-research and Labs modes.
Enterprise Pro$40/mo per seatPer user per month, or $400 per seat per year. Discounted rates are advertised for education and nonprofits.
Enterprise Maxquote onlySecondary sources put this near $325 per seat per month; the vendor directs buyers to sales, so it is recorded as quote-only.

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

Replaced by Vane

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

An answering engine with citations, assembled from a SearXNG you run and a model key you already pay for. Renamed from Perplexica in 2026.

The only one here that reproduces the actual shape of the product: you ask a question, it searches, and the answer arrives with numbered citations under it, in an interface built for that and nothing else. What you take on is both halves of what Perplexity pays for. The index becomes a SearXNG asking public engines that increasingly refuse datacenter addresses, and the model becomes an API key metered to you. It is the honest trade, not a free lunch.

What else we looked at

Ranked, with the reason each one placed where it did. Only the pick has a prompt on this page — the runners-up are named so the choice is visible, not hidden.

  1. Vaneour pickONE EVENING— setup effort 2 of 4

    An answering engine with citations, assembled from a SearXNG you run and a model key you already pay for. Renamed from Perplexica in 2026.

    The only one here that reproduces the actual shape of the product: you ask a question, it searches, and the answer arrives with numbered citations under it, in an interface built for that and nothing else. What you take on is both halves of what Perplexity pays for. The index becomes a SearXNG asking public engines that increasingly refuse datacenter addresses, and the model becomes an API key metered to you. It is the honest trade, not a free lunch.

  2. LibreChatONE EVENING— setup effort 2 of 4

    One chat window in front of Anthropic, OpenAI and Google, with the conversation history in a database you own.

    The better pick if what you actually wanted was the chat window, and search is occasional. It is a conversation-first interface over Anthropic, OpenAI and Google with a searchable history in your own database, and search arrives through plugins rather than being the point. Choose it when you would miss the threads more than the citations.

The swap

You're paying

Perplexity

$20/mo · $240/yr

is replaced by

You'd run

Vane

ONE EVENING · ~90 min to running · 2048 MB RAM

Perplexity Pro · vendor list price · checked 2026-08-06 · source · confidence: medium

Before you start

RAM floor
2048 MBfloor from upstream docs — not measured by us yet
Disk
5 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
~90 min1–3 hours, through the first backup

The prompt

Two paths to the same Vane: 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

344 lines · 14,995 bytes

What this prompt will do
  1. Preflight
  2. Layout
  3. Secrets
  4. Config and 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 Vane v1.12.2 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 this to the user first. The project was called Perplexica until it was renamed Vane, and
1.12.2 exists only under the new name. Vane is an interface, not a model: it searches with
SearXNG and writes the answer with a provider key the user supplies on its setup screen,
metered per token and billed to them.

Vane with SearXNG needs 2048 MB of RAM available and 5 GB free on /srv. Both images publish
amd64 and arm64. Measure five things:

```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>
caddy version
```

If available RAM is under 2048 MB or free disk is under 5 GB, print both numbers and stop. Do
not install and hope. If `dig +short` prints nothing, print that and stop. `caddy version` must
print 2.8 or newer, which is where `basicauth` was renamed `basic_auth`, the directive step 5
uses. If it is older, stop and tell the user to upgrade Caddy first.

## 2. Layout

```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/perplexica /srv/perplexica/backups
sudo install -d -m 700 /srv/perplexica/data /srv/perplexica/uploads
sudo install -d -m 755 /srv/perplexica/searxng
ls -la /srv/perplexica
```

Assert: `backups` owned by the login user, `data` and `uploads` at mode `700` owned by root
because the Vane image declares no unprivileged user, `searxng` at `755` because the one config
file in it holds no secret.

## 3. Secrets

Two secrets, both generated here. Print neither, repeat neither in your summary, and put
neither in a log line. The first replaces a session key published in the settings file every
copy of the bundled SearXNG image carries. The second is the password on the login box in step
5, the only thing between the internet and a setup screen holding the user's API key.

```bash
umask 077
cat > /srv/perplexica/.env <<EOF
SEARXNG_SECRET=$(openssl rand -hex 32)
EOF
openssl rand -hex 24 > /srv/perplexica/browser-login
chmod 600 /srv/perplexica/.env /srv/perplexica/browser-login
umask 022
ls -l /srv/perplexica/.env /srv/perplexica/browser-login
```

Assert: both files exist with mode `-rw-------`. Hex rather than base64, because the login value
gets typed into a browser dialog, and it is deliberately not in `.env`, which is what compose
hands a container. Neither is a provider API key: that arrives in the browser in step 7 and this
install never sees it.

## 4. Config and compose.yml

SearXNG answers in HTML only until its settings say otherwise, and Vane asks for JSON. Write
that file first:

```bash
sudo tee /srv/perplexica/searxng/settings.yml >/dev/null <<'EOF'
# SearXNG · the search backend here. Authored by caniselfhostit from
# https://docs.searxng.org/admin/settings/ and Vane's own install notes.
# No secret_key here. SEARXNG_SECRET in compose.yml overwrites it with the
# value generated in step 3, so nothing in this file is confidential.
use_default_settings: true

search:
  # SearXNG answers 403 to a format it was not told to serve, and ships html
  # only, so without json every Vane search fails.
  formats:
    - html
    - json

server:
  # The shipped default, stated so an upstream change cannot turn it on.
  limiter: false

engines:
  # Named in Vane's install notes.
  - name: wolframalpha
    disabled: false
EOF
sudo chmod 644 /srv/perplexica/searxng/settings.yml
```

Then the compose file:

```bash
cat > /srv/perplexica/compose.yml <<'EOF'
# Vane · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
#   install and images . https://github.com/ItzCrazyKns/Vane/blob/v1.12.2/README.md
#   searxng in docker .. https://docs.searxng.org/admin/installation-docker.html
#   searxng settings ... https://docs.searxng.org/admin/settings/settings_server.html
#
# Perplexica was renamed Vane; 1.12.2 exists only under the new name. Two
# services: the app, and the SearXNG it searches through. The slim image has no
# search engine in it; the full one bundles SearXNG and ships a fixed
# secret_key every copy of it shares. Apart, the key is generated and SearXNG
# updates on its own schedule. No provider API key is here: Vane asks for one
# on its setup screen and writes it to data/config.json, so that file is as
# sensitive as a password. Digests read 2026-08-06, both images multi-arch.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  searxng:
    image: searxng/searxng:2026.8.4-c63835bd2@sha256:f4c8e59de166ed71f6380c0847c312ca51f0d41996e31d0559163b6b09ecde52
    container_name: vane-searxng
    restart: unless-stopped
    environment:
      # Overwrites server.secret_key in settings.yml with the generated value.
      SEARXNG_SECRET: ${SEARXNG_SECRET}
    volumes:
      # Read only. The image owns /etc/searxng, so nothing is chowned here.
      - /srv/perplexica/searxng/settings.yml:/etc/searxng/settings.yml:ro
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:8080/healthz"]
      interval: 10s
      retries: 12
    # No `ports:` at all: 8080 is reachable only from the other container.

  vane:
    image: itzcrazykns1337/vane:slim-v1.12.2@sha256:d2878cf9c91962aa3fc053b59bc9b89adcbdcaeb7ee36b54906e853464b2c190
    container_name: vane
    restart: unless-stopped
    environment:
      # Vane appends /search?format=json to this address on every query.
      SEARXNG_API_URL: http://searxng:8080
    volumes:
      # config.json, the SQLite database of searches, and uploaded files.
      - /srv/perplexica/data:/home/vane/data
      - /srv/perplexica/uploads:/home/vane/uploads
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8148.
      - "127.0.0.1:8148:3000"
    depends_on:
      searxng:
        condition: service_healthy
EOF
cd /srv/perplexica && docker compose config >/dev/null && echo "compose OK"
```

Assert: that prints `compose OK`. Compose reads `.env` for `${SEARXNG_SECRET}` because it sits
beside this file, and 8148 is the only published port.

## 5. Caddy and TLS

Two files. First the credential Caddy checks: a bcrypt hash of the step 3 password, written
where the caddy user can read it and nowhere else.

```bash
umask 077
caddy hash-password < /srv/perplexica/browser-login > /srv/perplexica/vane-auth.hash
printf 'basic_auth {\n\tvane %s\n}\n' "$(cat /srv/perplexica/vane-auth.hash)" > /srv/perplexica/vane-auth.conf
umask 022
sudo install -m 640 -o root -g caddy /srv/perplexica/vane-auth.conf /etc/caddy/vane-auth.conf
rm -f /srv/perplexica/vane-auth.hash /srv/perplexica/vane-auth.conf
sudo grep -c basic_auth /etc/caddy/vane-auth.conf
```

Assert: that prints `1`. The username is the literal `vane`. Reading the password from a file
keeps it off the process list.

Then the site block, with `<DOMAIN>` replaced by the real hostname. Copy the Caddyfile first: a
syntax error here takes every site on the box down.

```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-perplexica
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# Vane · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://github.com/ItzCrazyKns/Vane/blob/v1.12.2/README.md,
# https://caddyserver.com/docs/automatic-https and
# https://caddyserver.com/docs/caddyfile/directives/basic_auth
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname pointed at this box. Vane has no
# sign-in of its own, so this block is the login: the setup screen behind it
# holds the API key every answer is billed to. Needs Caddy 2.8 or newer, where
# the directive is spelled basic_auth.

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

	# The credential is not in this file, because this file is published. The
	# install writes /etc/caddy/vane-auth.conf: one basic_auth block with the
	# username `vane` and a bcrypt hash of the generated password.
	import /etc/caddy/vane-auth.conf

	# 8148 is the loopback port compose publishes on this host. Not a container
	# port, and not open in the firewall.
	reverse_proxy 127.0.0.1:8148 {
		# Answers arrive one piece at a time: no `encode` line anywhere here,
		# and a proxy that flushes every write instead of buffering them.
		flush_interval -1
	}
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```

Assert: `caddy validate` exits 0 and the reload exits 0. If validate fails, restore
/etc/caddy/Caddyfile.before-perplexica, reload, and report what it objected to. Caddy gets the
certificate on the first request and renews it alone.

## 6. Firewall

Two ports open, both Caddy's. Idempotent, so on a Prompt Zero box 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, 443/udp
is HTTP/3. 8148 stays closed because it is bound to 127.0.0.1, and opening it would route
around the login box in step 5. SearXNG publishes no host port at all. Assert: `ufw status
verbose` prints `Status: active`, shows those three, and no rule for 8148 or 8080.

## 7. Start and verify

The Vane image is about a gigabyte, so the pull takes minutes.

```bash
cd /srv/perplexica
docker compose pull
docker compose up -d
for i in $(seq 1 40); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:8148/); echo "$i $code"; [ "$code" = 200 ] && break; sleep 10; done
curl -sS http://127.0.0.1:8148/api/config | grep -oE '"setupComplete":[a-z]*|"searxngURL":"[^"]*"'
curl -sS http://127.0.0.1:8148/ | grep -c 'Welcome to'
docker compose exec -T searxng wget -qO- 'http://127.0.0.1:8080/search?q=self+hosting&format=json' | grep -c '"query"'
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/
```

Assert all five, and print what you received for each. The loop ends printing `200`. The grep
prints `"setupComplete":false` and the searxng URL. The first grep
prints `1`, the setup screen. The SearXNG grep prints `1`, proving JSON is enabled and step 4's
file was read; a `403` there means it was not. The last curl prints `401`, Caddy refusing an
unauthenticated request, and that is the security assert here. If any of the five misses, stop,
run `docker compose logs --tail 40 vane` and `docker compose logs --tail 20 searxng`, and name
the likely earlier step. A running container is not success.

STOP: tell the user to open https://<DOMAIN>, sign in with the username `vane` and the password
they read with `sudo cat /srv/perplexica/browser-login`, and wait. Do not continue until they
confirm. The first screen reads `Welcome to Vane` over `Web search, reimagined`, then the setup
wizard asks for a model provider. Tell them to put that password in their password manager,
paste their own provider key into the wizard, finish it, and run one search. Do not report
success until they confirm an answer came back with numbered citations under it. That key is
theirs and billed to their account; never ask them to paste it to you.

## 8. First backup and restore

One archive, and the container stops for it: past searches are a SQLite file, and a copy taken
mid-write is not a backup.

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

Assert: the archive exists and is non-empty. Print its size. Downtime is about ten seconds. It
holds `data/config.json` and therefore the provider key, so it is as sensitive as a password
file. A backup on the same disk is not one, so run this from the user's machine:

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

To restore: `docker compose down`, `sudo rm -rf /srv/perplexica/data /srv/perplexica/uploads`,
untar the archive back into /srv/perplexica, re-run the first fence of step 5 to rebuild
/etc/caddy/vane-auth.conf from the restored `browser-login`, `sudo systemctl reload caddy`,
then `docker compose up -d` and re-run step 7's five asserts. That is the whole disaster plan,
and this archive is the only copy of the key they pasted in.

## 9. Updating later

Vane releases are at https://github.com/ItzCrazyKns/Vane/releases, and a slim tag is a release
tag with `slim-` in front of it. SearXNG publishes a dated tag most days at
https://hub.docker.com/r/searxng/searxng/tags. Back up first, then edit the `image:` line you
are changing in /srv/perplexica/compose.yml to its new tag and digest:

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

Move the two images on their own schedules. Vane migrates its database on the way up, so watch
that log until it settles. Bump the SearXNG tag when searches come back thin: that is usually
an engine upstream has already fixed.

## 10. What will probably go wrong

Searches will work in a browser and come back half empty here, and it looks like a broken
install. It is not. SearXNG asks the real engines on your behalf and the real engines block
datacenter addresses: Google in particular blocks fresh instances within a handful of searches,
SearXNG suspends that engine for an hour, and the answer gets written from whatever survived. I
read thin, oddly-sourced answers for a day before I understood this is the deal rather than a
fault, and it is the honest difference between this and the product it replaces, which pays for
an index. Run
`docker compose exec -T searxng wget -qO- 'http://127.0.0.1:8080/stats/errors'` first.

## 11. Out of scope

- Do not switch to the full Vane image for its bundled SearXNG. That image carries a fixed key
  every copy of it shares, and its SearXNG only moves when Vane cuts a release.
- Do not put a provider API key in `.env` or compose.yml. It belongs on the setup screen.
- Do not remove the `import` line from the Caddy block and do not open 8148. Vane has no login
  of its own, and without that box anyone who finds the hostname spends the user's money.
- Do not add Valkey, Redis or the SearXNG limiter. Those are for instances the public reaches.
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 Vane v1.12.2 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.

Two facts before step 1. The project was called Perplexica until it was renamed Vane, so the
repository, the images and the docs all say Vane now, and 1.12.2 exists only under that name.
And Vane is an interface, not a model: it searches with SearXNG and writes the answer with a
provider key you supply on its setup screen, metered per token by Anthropic, OpenAI or Google
and billed to you. Nothing here caps that.

## 1. Preflight

```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>
caddy version
```

You should see: at least `2048` MB available, at least `5` G free, `amd64` or `arm64`, your
server's IP, and a Caddy version of `2.8` or newer.

If you do not: an empty `dig` line means the A record does not exist yet. Add it, wait a
minute, run `dig +short <DOMAIN>` again, because Caddy cannot get a certificate for a hostname
that does not resolve and failed attempts count against a rate limit you cannot see. A Caddy
older than 2.8 is a hard stop: step 5 uses the `basic_auth` directive, which 2.8 renamed from
`basicauth`, and on an older build the config will not validate. Upgrade Caddy first.

## 2. Layout

```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/perplexica /srv/perplexica/backups
sudo install -d -m 700 /srv/perplexica/data /srv/perplexica/uploads
sudo install -d -m 755 /srv/perplexica/searxng
ls -la /srv/perplexica
```

You should see: `backups` owned by you, `data` and `uploads` at `drwx------` owned by root, and
`searxng` at `drwxr-xr-x`.

If you do not: leave `data` and `uploads` owned by root on purpose. The Vane image declares no
unprivileged user and writes as root, so a directory you have chowned to yourself is a
permission error at first start. `searxng` is deliberately readable, because the one file in it
holds no secret.

## 3. Secrets

Two secrets, both generated here on the server and never typed by you. The first replaces a
SearXNG session key whose value is published in the settings file every copy of the bundled
image carries. The second is the password on the login box in step 5.

```bash
umask 077
cat > /srv/perplexica/.env <<EOF
SEARXNG_SECRET=$(openssl rand -hex 32)
EOF
openssl rand -hex 24 > /srv/perplexica/browser-login
chmod 600 /srv/perplexica/.env /srv/perplexica/browser-login
umask 022
ls -l /srv/perplexica/.env /srv/perplexica/browser-login
```

You should see: two files at mode `-rw-------`, your own username twice on each line. Read the
login password once with `sudo cat /srv/perplexica/browser-login` and put it in your password
manager: with the username `vane` it is how you get into this install.

If you do not: a mode of `-rw-r--r--` means `umask 077` did not take effect, which happens if
you pasted the lines separately in different shells. Run
`chmod 600 /srv/perplexica/.env /srv/perplexica/browser-login` and carry on.

Do not paste either file, either value, or any output containing them into this chat window.
Neither of them is your provider API key: that one you type into Vane's own setup screen in
step 7, and it never passes through here either.

## 4. Config and compose.yml

SearXNG answers in HTML only until its settings say otherwise, and Vane asks for JSON. Paste
this whole block at once, including the last line.

```bash
sudo tee /srv/perplexica/searxng/settings.yml >/dev/null <<'EOF'
# SearXNG · the search backend here. Authored by caniselfhostit from
# https://docs.searxng.org/admin/settings/ and Vane's own install notes.
# No secret_key here. SEARXNG_SECRET in compose.yml overwrites it with the
# value generated in step 3, so nothing in this file is confidential.
use_default_settings: true

search:
  # SearXNG answers 403 to a format it was not told to serve, and ships html
  # only, so without json every Vane search fails.
  formats:
    - html
    - json

server:
  # The shipped default, stated so an upstream change cannot turn it on.
  limiter: false

engines:
  - name: wolframalpha
    disabled: false
EOF
sudo chmod 644 /srv/perplexica/searxng/settings.yml
```

You should see: no output at all, which is what `>/dev/null` is for.

If you do not: an error mentioning `No such file or directory` means step 2 did not run. Go
back and create the tree first.

Now the compose file. Paste the whole block at once, including the last two lines.

```bash
cat > /srv/perplexica/compose.yml <<'EOF'
# Vane · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
#   install and images . https://github.com/ItzCrazyKns/Vane/blob/v1.12.2/README.md
#   searxng in docker .. https://docs.searxng.org/admin/installation-docker.html
#   searxng settings ... https://docs.searxng.org/admin/settings/settings_server.html
#
# Perplexica was renamed Vane; 1.12.2 exists only under the new name. Two
# services: the app, and the SearXNG it searches through. The slim image has no
# search engine in it; the full one bundles SearXNG and ships a fixed
# secret_key every copy of it shares. Apart, the key is generated and SearXNG
# updates on its own schedule. No provider API key is here: Vane asks for one
# on its setup screen and writes it to data/config.json, so that file is as
# sensitive as a password. Digests read 2026-08-06, both images multi-arch.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  searxng:
    image: searxng/searxng:2026.8.4-c63835bd2@sha256:f4c8e59de166ed71f6380c0847c312ca51f0d41996e31d0559163b6b09ecde52
    container_name: vane-searxng
    restart: unless-stopped
    environment:
      # Overwrites server.secret_key in settings.yml with the generated value.
      SEARXNG_SECRET: ${SEARXNG_SECRET}
    volumes:
      # Read only. The image owns /etc/searxng, so nothing is chowned here.
      - /srv/perplexica/searxng/settings.yml:/etc/searxng/settings.yml:ro
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:8080/healthz"]
      interval: 10s
      retries: 12
    # No `ports:` at all: 8080 is reachable only from the other container.

  vane:
    image: itzcrazykns1337/vane:slim-v1.12.2@sha256:d2878cf9c91962aa3fc053b59bc9b89adcbdcaeb7ee36b54906e853464b2c190
    container_name: vane
    restart: unless-stopped
    environment:
      # Vane appends /search?format=json to this address on every query.
      SEARXNG_API_URL: http://searxng:8080
    volumes:
      # config.json, the SQLite database of searches, and uploaded files.
      - /srv/perplexica/data:/home/vane/data
      - /srv/perplexica/uploads:/home/vane/uploads
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8148.
      - "127.0.0.1:8148:3000"
    depends_on:
      searxng:
        condition: service_healthy
EOF
cd /srv/perplexica && docker compose config >/dev/null && echo "compose OK"
```

You should see: `compose OK` and nothing else.

If you do not: `services must be a mapping` means the indentation was lost between the page and
your terminal, so run `rm /srv/perplexica/compose.yml` and paste again in one go. A warning
about `SEARXNG_SECRET` being unset means step 3's `.env` is missing or is not in
/srv/perplexica, which is where compose looks for it.

## 5. Caddy and TLS

Two files. First the credential Caddy checks: a bcrypt hash of the password from step 3.
Reading it from a file rather than typing it as an argument keeps it off the process list.

```bash
umask 077
caddy hash-password < /srv/perplexica/browser-login > /srv/perplexica/vane-auth.hash
printf 'basic_auth {\n\tvane %s\n}\n' "$(cat /srv/perplexica/vane-auth.hash)" > /srv/perplexica/vane-auth.conf
umask 022
sudo install -m 640 -o root -g caddy /srv/perplexica/vane-auth.conf /etc/caddy/vane-auth.conf
rm -f /srv/perplexica/vane-auth.hash /srv/perplexica/vane-auth.conf
sudo grep -c basic_auth /etc/caddy/vane-auth.conf
```

You should see: `1`.

If you do not: `chown: invalid group: 'caddy'` means Caddy was installed some other way and its
service user has a different name. Run `systemctl show -p User caddy` to find it and use that
name in the `install` line. A `0` means the printf did not run, usually because the hash file
was empty; check that `sudo cat /srv/perplexica/browser-login` prints 48 hex characters.

Now the site block. Replace `<DOMAIN>` with your hostname before you paste. The first line takes
a copy, because a syntax error here takes down every other site on the box.

```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-perplexica
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# Vane · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://github.com/ItzCrazyKns/Vane/blob/v1.12.2/README.md,
# https://caddyserver.com/docs/automatic-https and
# https://caddyserver.com/docs/caddyfile/directives/basic_auth
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname pointed at this box. Vane has no
# sign-in of its own, so this block is the login: the setup screen behind it
# holds the API key every answer is billed to. Needs Caddy 2.8 or newer, where
# the directive is spelled basic_auth.

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

	# The credential is not in this file, because this file is published. The
	# install writes /etc/caddy/vane-auth.conf: one basic_auth block with the
	# username `vane` and a bcrypt hash of the generated password.
	import /etc/caddy/vane-auth.conf

	# 8148 is the loopback port compose publishes on this host. Not a container
	# port, and not open in the firewall.
	reverse_proxy 127.0.0.1:8148 {
		# Answers arrive one piece at a time: no `encode` line anywhere here,
		# and a proxy that flushes every write instead of buffering them.
		flush_interval -1
	}
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```

You should see: `Valid configuration` from validate, and no output at all from reload.

If you do not: run `sudo cp /etc/caddy/Caddyfile.before-perplexica /etc/caddy/Caddyfile`,
reload, and paste again. `unrecognized directive: basic_auth` inside the imported file means
your Caddy predates 2.8, which step 1 was checking for.

## 6. Firewall

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

You should see: `Status: active`, rules for `80/tcp`, `443/tcp` and `443/udp`, and no rule
mentioning `8148` or `8080`.

If you do not: delete anything for `8148` with `sudo ufw delete allow 8148`. That port is bound
to 127.0.0.1 by the compose file, and opening it would let anyone reach Vane without passing
the login box you built in step 5. SearXNG never publishes a host port at all. `Status:
inactive` is a different problem: Prompt Zero left this firewall on, so something has turned it
off since, and `sudo ufw enable` puts it back before you go further.

## 7. Start and verify

The Vane image is around a gigabyte, so the first pull takes a few minutes.

```bash
cd /srv/perplexica
docker compose pull
docker compose up -d
for i in $(seq 1 40); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:8148/); echo "$i $code"; [ "$code" = 200 ] && break; sleep 10; done
curl -sS http://127.0.0.1:8148/api/config | grep -oE '"setupComplete":[a-z]*|"searxngURL":"[^"]*"'
curl -sS http://127.0.0.1:8148/ | grep -c 'Welcome to'
docker compose exec -T searxng wget -qO- 'http://127.0.0.1:8080/search?q=self+hosting&format=json' | grep -c '"query"'
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/
```

You should see, in order: the loop reaching `200`; JSON containing `"setupComplete":false` and
`"searxngURL":"http://searxng:8080"`; then `1`; then `1`; then `401`.

If you do not: the `401` is the one worth understanding. It means Caddy is up and refusing a
request with no credentials, which is exactly what you want on a public hostname, so seeing it
is good news. A `200` in its place means the `import` line is not being read and your install
is open to the internet: fix that before anything else. If the SearXNG grep prints `0`, run the
same wget without the grep; a `403` body means /srv/perplexica/searxng/settings.yml was not
picked up, so check the mount path in step 4. If the loop never reaches `200`, run
`docker compose logs --tail 40 vane` and `docker compose logs --tail 20 searxng`.

Open https://<DOMAIN> in a browser. The username is `vane` and the password is the one you read
in step 3. The first screen reads `Welcome to Vane` over `Web search, reimagined`, then a setup
wizard asks for a model provider. Paste your own provider API key into it, finish the wizard,
and run one search. You are done when an answer comes back with numbered citations under it,
not when the containers are running.

## 8. First backup and restore

The container stops for this: past searches are a SQLite file, and a copy taken mid-write is
not a backup.

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

You should see: one archive, tens of kilobytes on a fresh install, and about ten seconds of
downtime.

If you do not: `tar: data: Cannot stat` means step 2 never made the directory. An archive of
about 100 bytes means everything it was asked for was missing, so read the `tar` output rather
than the size.

That archive holds `data/config.json`, and therefore your provider API key, so treat it like a
password file. A backup on the same disk is not a backup. Run this on your own machine, not the
server:

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

You should see: one file copied, and it listed by `ls -lh ~/backups/perplexica/`.

If you do not: `Permission denied (publickey)` means you ran it on the server. The `vps:` prefix
only means something on your own machine, where the alias Prompt Zero created lives.

Now prove the restore, today, while the only thing at risk is a test search:

```bash
cd /srv/perplexica
docker compose down
sudo rm -rf /srv/perplexica/data /srv/perplexica/uploads
sudo tar -xzf /srv/perplexica/backups/perplexica-$(date +%F).tar.gz -C /srv/perplexica data uploads
docker compose up -d
sleep 20
curl -sS http://127.0.0.1:8148/api/config | grep -oE '"setupComplete":[a-z]*|"searxngURL":"[^"]*"'
```

You should see: the config JSON again, this time with `"setupComplete":true`, which means the
setup you did in step 7 survived deleting and rebuilding the data directory.

If you do not: `"setupComplete":false` means the untar put the files somewhere else. Run
`sudo tar -tzf` on the archive to see the paths it actually contains. Understand the stakes
before you skip this step: the key you pasted into the setup screen lives in that archive and
nowhere else you control.

## 9. Updating later

Vane releases are at https://github.com/ItzCrazyKns/Vane/releases, and a slim tag is a release
tag with `slim-` in front of it. SearXNG publishes a dated tag most days at
https://hub.docker.com/r/searxng/searxng/tags. Take the backup first, then edit the `image:`
line you are changing in /srv/perplexica/compose.yml to its new tag and digest.

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

You should see: migration lines, then the server starting, and no repeating restart.

If you do not: put the old tag and digest back and run the same three commands. Move the two
images on their own schedules rather than together, and bump the SearXNG tag whenever searches
start coming back thin, because that is usually an engine upstream has already fixed.

## 10. What will probably go wrong

Searches will work in a browser and come back half empty here, and it looks like a broken
install. It is not. SearXNG asks the real engines on your behalf and the real engines block
datacenter addresses: Google in particular blocks fresh instances within a handful of searches,
SearXNG suspends that engine for an hour, and the answer gets written from whatever survived. I
read thin, oddly-sourced answers for a day before I understood this is the deal rather than a
fault, and it is the honest difference between this and the product it replaces, which pays for
an index. Run
`docker compose exec -T searxng wget -qO- 'http://127.0.0.1:8080/stats/errors'` first.

## 11. Out of scope

- Do not switch to the full Vane image for its bundled SearXNG. That image carries a fixed key
  every copy of it shares, and its SearXNG only moves when Vane cuts a release.
- Do not put a provider API key in `.env` or compose.yml. It belongs on the setup screen.
- Do not remove the `import` line from the Caddy block and do not open 8148. Vane has no login
  of its own, and without that box anyone who finds the hostname spends your money.
- Do not add Valkey, Redis or the SearXNG limiter. Those are for instances the public reaches.

328 lines · 14,983 bytes

What this prompt will do
  1. Preflight
  2. Docker
  3. Layout
  4. Secrets
  5. compose.yml
  6. Nothing is public
  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 Vane v1.12.2 and the SearXNG it searches through under ~/selfhost/perplexica,
answering at http://localhost:8148.

## 1. Preflight

Say this to the user before step 2; it decides whether they want this install at all. The
project was Perplexica until it was renamed Vane, and 1.12.2 exists only under the new name. It
answers at http://localhost:8148, this computer and nowhere else: their phone cannot open it
and nothing runs while the lid is shut. And Vane is an interface, not a model: it searches with
SearXNG and writes the answer with a provider key they supply on its setup screen, metered per
token and billed to them.

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. Vane plus SearXNG needs 2048 MB of RAM
available and 5 GB free on the home disk, and both images publish amd64 and arm64. Every branch
prints free memory; on macOS and Windows it is the host's, and Docker Desktop takes its
allocation out of it. If available RAM is under 2048 MB or free disk is under 5 GB, print both
numbers and stop. Do not install and hope.

## 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/perplexica/data ~/selfhost/perplexica/uploads ~/selfhost/perplexica/searxng ~/selfhost/perplexica/backups
ls -la ~/selfhost/perplexica
```

Assert: four directories, owned by the user. No ownership fix runs here. The Vane image declares
no unprivileged user, so on Linux the files it writes inside `data` belong to root and are read
with `sudo`; Docker Desktop handles that on macOS and Windows.

## 4. Secrets

One secret here. Generate it, print it nowhere, and keep it out of your summary and out of
any log line.

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

Assert: the file exists with mode `-rw-------`. Git Bash ships openssl, so this runs the same on
all three systems. SearXNG otherwise falls back to a key published in the settings file every
copy of the bundled image carries. The server path generates a second secret, the password on
its login box; this path has none, and step 6 is why. This is not a provider API key: that
arrives in the browser in step 7.

On Windows those mode bits are advisory: NTFS does not enforce them, and the real boundary is
the user's own Windows account.

## 5. compose.yml

SearXNG answers in HTML only until told otherwise, and Vane asks for JSON. Write that file
first:

```bash
cat > ~/selfhost/perplexica/searxng/settings.yml <<'EOF'
# SearXNG · the search backend here. Authored by caniselfhostit from
# https://docs.searxng.org/admin/settings/ and Vane's own install notes. No
# secret_key here: SEARXNG_SECRET in compose.yml overwrites it with the step 4
# value, so nothing in this file is confidential.
use_default_settings: true

search:
  # SearXNG answers 403 to a format it was not told to serve, and ships html
  # only, so without json every Vane search fails.
  formats:
    - html
    - json

server:
  # The shipped default, stated so an upstream change cannot turn it on.
  limiter: false

engines:
  - name: wolframalpha
    disabled: false
EOF
```

Then the compose file:

```bash
cat > ~/selfhost/perplexica/compose.yml <<'EOF'
# Vane · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
#   install and images . https://github.com/ItzCrazyKns/Vane/blob/v1.12.2/README.md
#   searxng in docker .. https://docs.searxng.org/admin/installation-docker.html
#   searxng settings ... https://docs.searxng.org/admin/settings/settings_server.html
#
# Perplexica was renamed Vane; 1.12.2 exists only under the new name. Two
# services on the computer you are sitting at, every path relative to
# ~/selfhost/perplexica/, which lets one file work on macOS, Linux and Windows.
# No named volumes: neither image chowns a directory it is handed. The slim
# image has no search engine in it; the full one bundles SearXNG and ships a
# fixed secret_key every copy of it shares. No provider API key is here: Vane
# asks for one on its setup screen and writes it to data/config.json. Digests
# read 2026-08-06, both images multi-arch.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  searxng:
    image: searxng/searxng:2026.8.4-c63835bd2@sha256:f4c8e59de166ed71f6380c0847c312ca51f0d41996e31d0559163b6b09ecde52
    container_name: vane-searxng
    restart: unless-stopped
    environment:
      # Overwrites server.secret_key in settings.yml with the generated value.
      SEARXNG_SECRET: ${SEARXNG_SECRET}
    volumes:
      # Read only. The image owns /etc/searxng, so nothing is chowned here.
      - ./searxng/settings.yml:/etc/searxng/settings.yml:ro
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:8080/healthz"]
      interval: 10s
      retries: 12
    # No `ports:` at all: 8080 is reachable only from the other container.

  vane:
    image: itzcrazykns1337/vane:slim-v1.12.2@sha256:d2878cf9c91962aa3fc053b59bc9b89adcbdcaeb7ee36b54906e853464b2c190
    container_name: vane
    restart: unless-stopped
    environment:
      # Vane appends /search?format=json to this address on every query.
      SEARXNG_API_URL: http://searxng:8080
    volumes:
      # config.json, the SQLite database of searches, and uploaded files.
      - ./data:/home/vane/data
      - ./uploads:/home/vane/uploads
    ports:
      # Loopback only: no other device on the wifi can reach 8148.
      - "127.0.0.1:8148:3000"
    depends_on:
      searxng:
        condition: service_healthy
EOF
cd ~/selfhost/perplexica && docker compose config >/dev/null && echo "compose OK"
```

Assert: that prints `compose OK`. Two services, one published port, three binds.

## 6. Nothing is public

No reverse proxy, no certificate, no firewall rule. Each is a decision.

- No DNS, so nothing to resolve and nothing to wait for.
- No TLS. A certificate attests a public name and nothing here has one. Browsers treat
  http://localhost as a secure context anyway, so pages needing crypto still work.
- No login box. The server path puts one in front of this, because an open Vane on a public
  hostname is a stranger spending the user's money. Here the machine is the boundary.

8148 is bound to 127.0.0.1, this computer only. Not the user's phone, not a laptop on the same
wifi, not anyone on the internet. Confirm it:

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

Assert: that prints `1`, the published port `"127.0.0.1:8148:3000"`. SearXNG publishes no host
port, so 8080 cannot appear. One quiet advantage here: searches leave from a home address, not
a datacenter, and the engines SearXNG asks are far less likely to refuse them.

## 7. Start and verify

The Vane image is around a gigabyte, so the first pull takes minutes.

```bash
cd ~/selfhost/perplexica
docker compose pull
docker compose up -d
for i in $(seq 1 40); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://localhost:8148/); echo "$i $code"; [ "$code" = 200 ] && break; sleep 10; done
curl -sS http://localhost:8148/api/config | grep -oE '"setupComplete":[a-z]*|"searxngURL":"[^"]*"'
curl -sS http://localhost:8148/ | grep -c 'Welcome to'
docker compose exec -T searxng wget -qO- 'http://127.0.0.1:8080/search?q=self+hosting&format=json' | grep -c '"query"'
```

Assert all four, and print what you received for each. The loop ends printing `200`. The grep
prints `"setupComplete":false` and the searxng URL. The first grep
prints `1`, the setup screen. The SearXNG grep prints `1`, proving JSON is enabled and step 5's
settings file was read; a `403` means it was not. If any of the four misses, stop, run
`docker compose logs --tail 40 vane` and `docker compose logs --tail 20 searxng`, and name the
likely cause. If `port is already allocated` came back, find what holds 8148 with
`lsof -nP -iTCP:8148 -sTCP:LISTEN` and stop until the user frees it.
A running container is not success.

STOP: tell the user to open http://localhost:8148 and wait. Do not continue until they confirm.
The first screen reads `Welcome to Vane` over `Web search, reimagined`, then the setup wizard
asks for a model provider. Tell them to paste their own provider API key into it, finish the
wizard, and run one search. Do not report success until they confirm an answer came back with
numbered citations. That key is theirs and billed to their account; never ask for it.

## 8. First backup and restore

One archive, and the container stops for it: past searches are a SQLite file, and a copy taken
mid-write is not one.

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

Assert: the archive exists and is non-empty. Print its size. Downtime is about ten seconds. On
Linux the `data` files belong to root, so `Cannot open: Permission denied` from `tar` means
running that one line with `sudo`.

The archive holds `data/config.json`, and therefore the provider API key, so it is as sensitive
as a password file. It sits on the same disk as the data, which is not a backup: on a laptop
the disk and the machine fail together. Ask the user for a destination that leaves this
computer, a folder their sync service watches or a USB stick, and copy it there with `cp`. In
Git Bash a Windows drive is `/d/Backups`. Assert: the user confirms the filename is there. If
they have nowhere, say plainly that this has no backup.

To restore: `cd ~/selfhost/perplexica`, `docker compose down`, `rm -rf data uploads`, untar the
archive back into ~/selfhost/perplexica, then `docker compose up -d` and re-run step 7's four
asserts. That is the whole disaster plan.

## 9. Updating later

Vane releases are at https://github.com/ItzCrazyKns/Vane/releases, and a slim tag is a release
tag with `slim-` in front. SearXNG publishes a dated tag most days at
https://hub.docker.com/r/searxng/searxng/tags. Back up first, then edit the `image:` line you
are changing in ~/selfhost/perplexica/compose.yml to its new tag and digest:

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

Move the two images on their own schedules. Vane migrates its database on the way up, so watch
that log until it settles.

## 10. What will probably go wrong

I rebooted this machine, opened http://localhost:8148, and got a connection error that read
like a lost install. It was not: Docker Desktop had not started with the session, so
nothing was listening on 8148 until it did. `restart: unless-stopped` only acts once the Docker
daemon is up. Turn on start-at-login, and after a reboot run
`cd ~/selfhost/perplexica && docker compose up -d` before concluding anything is broken.

## 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 8148 to 0.0.0.0 so a phone on the wifi can reach it. Vane has no login of its
  own, and that puts a key-spending window on every network this machine joins.
- Do not switch to the full Vane image for its bundled SearXNG. That image carries a fixed key
  every copy of it shares.
- Do not put a provider API key in `.env` or compose.yml. It belongs on the setup screen.
compose.local.ymlthe services, pinned · local layout51 lines

authored from upstream docs, never pasted · 2,388 bytes

# Vane · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
#   install and images . https://github.com/ItzCrazyKns/Vane/blob/v1.12.2/README.md
#   searxng in docker .. https://docs.searxng.org/admin/installation-docker.html
#   searxng settings ... https://docs.searxng.org/admin/settings/settings_server.html
#
# Perplexica was renamed Vane; 1.12.2 exists only under the new name. Two
# services on the computer you are sitting at, every path relative to
# ~/selfhost/perplexica/, which lets one file work on macOS, Linux and Windows.
# No named volumes: neither image chowns a directory it is handed. The slim
# image has no search engine in it; the full one bundles SearXNG and ships a
# fixed secret_key every copy of it shares. No provider API key is here: Vane
# asks for one on its setup screen and writes it to data/config.json. Digests
# read 2026-08-06, both images multi-arch.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  searxng:
    image: searxng/searxng:2026.8.4-c63835bd2@sha256:f4c8e59de166ed71f6380c0847c312ca51f0d41996e31d0559163b6b09ecde52
    container_name: vane-searxng
    restart: unless-stopped
    environment:
      # Overwrites server.secret_key in settings.yml with the generated value.
      SEARXNG_SECRET: ${SEARXNG_SECRET}
    volumes:
      # Read only. The image owns /etc/searxng, so nothing is chowned here.
      - ./searxng/settings.yml:/etc/searxng/settings.yml:ro
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:8080/healthz"]
      interval: 10s
      retries: 12
    # No `ports:` at all: 8080 is reachable only from the other container.

  vane:
    image: itzcrazykns1337/vane:slim-v1.12.2@sha256:d2878cf9c91962aa3fc053b59bc9b89adcbdcaeb7ee36b54906e853464b2c190
    container_name: vane
    restart: unless-stopped
    environment:
      # Vane appends /search?format=json to this address on every query.
      SEARXNG_API_URL: http://searxng:8080
    volumes:
      # config.json, the SQLite database of searches, and uploaded files.
      - ./data:/home/vane/data
      - ./uploads:/home/vane/uploads
    ports:
      # Loopback only: no other device on the wifi can reach 8148.
      - "127.0.0.1:8148:3000"
    depends_on:
      searxng:
        condition: service_healthy

agent-readable mirror: /self-host/perplexity.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, pinned50 lines

authored from upstream docs, never pasted · 2,377 bytes

# Vane · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
#   install and images . https://github.com/ItzCrazyKns/Vane/blob/v1.12.2/README.md
#   searxng in docker .. https://docs.searxng.org/admin/installation-docker.html
#   searxng settings ... https://docs.searxng.org/admin/settings/settings_server.html
#
# Perplexica was renamed Vane; 1.12.2 exists only under the new name. Two
# services: the app, and the SearXNG it searches through. The slim image has no
# search engine in it; the full one bundles SearXNG and ships a fixed
# secret_key every copy of it shares. Apart, the key is generated and SearXNG
# updates on its own schedule. No provider API key is here: Vane asks for one
# on its setup screen and writes it to data/config.json, so that file is as
# sensitive as a password. Digests read 2026-08-06, both images multi-arch.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  searxng:
    image: searxng/searxng:2026.8.4-c63835bd2@sha256:f4c8e59de166ed71f6380c0847c312ca51f0d41996e31d0559163b6b09ecde52
    container_name: vane-searxng
    restart: unless-stopped
    environment:
      # Overwrites server.secret_key in settings.yml with the generated value.
      SEARXNG_SECRET: ${SEARXNG_SECRET}
    volumes:
      # Read only. The image owns /etc/searxng, so nothing is chowned here.
      - /srv/perplexica/searxng/settings.yml:/etc/searxng/settings.yml:ro
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:8080/healthz"]
      interval: 10s
      retries: 12
    # No `ports:` at all: 8080 is reachable only from the other container.

  vane:
    image: itzcrazykns1337/vane:slim-v1.12.2@sha256:d2878cf9c91962aa3fc053b59bc9b89adcbdcaeb7ee36b54906e853464b2c190
    container_name: vane
    restart: unless-stopped
    environment:
      # Vane appends /search?format=json to this address on every query.
      SEARXNG_API_URL: http://searxng:8080
    volumes:
      # config.json, the SQLite database of searches, and uploaded files.
      - /srv/perplexica/data:/home/vane/data
      - /srv/perplexica/uploads:/home/vane/uploads
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8148.
      - "127.0.0.1:8148:3000"
    depends_on:
      searxng:
        condition: service_healthy
Caddyfilethe hostname and TLS35 lines

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

# Vane · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://github.com/ItzCrazyKns/Vane/blob/v1.12.2/README.md,
# https://caddyserver.com/docs/automatic-https and
# https://caddyserver.com/docs/caddyfile/directives/basic_auth
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname pointed at this box. Vane has no
# sign-in of its own, so this block is the login: the setup screen behind it
# holds the API key every answer is billed to. Needs Caddy 2.8 or newer, where
# the directive is spelled basic_auth.

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

	# The credential is not in this file, because this file is published. The
	# install writes /etc/caddy/vane-auth.conf: one basic_auth block with the
	# username `vane` and a bcrypt hash of the generated password.
	import /etc/caddy/vane-auth.conf

	# 8148 is the loopback port compose publishes on this host. Not a container
	# port, and not open in the firewall.
	reverse_proxy 127.0.0.1:8148 {
		# Answers arrive one piece at a time: no `encode` line anywhere here,
		# and a proxy that flushes every write instead of buffering them.
		flush_interval -1
	}
}
install.shthe same install, no agent207 lines

authored from upstream docs, never pasted · 9,256 bytes

#!/usr/bin/env bash
# Vane · 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=ask.example.com ./install.sh
#
# Authored by caniselfhostit from the upstream documentation:
#   https://github.com/ItzCrazyKns/Vane/blob/v1.12.2/README.md
#   https://github.com/ItzCrazyKns/Vane/blob/v1.12.2/docs/installation/UPDATING.md
#   https://docs.searxng.org/admin/installation-docker.html
#   https://docs.searxng.org/admin/settings/settings_server.html
#   https://caddyserver.com/docs/caddyfile/directives/basic_auth
#
# The project was called Perplexica until it was renamed Vane. The repository,
# the images and the docs moved with the name; 1.12.2 exists only under the new
# one.
#
# Two secrets are generated here, on this machine: the SearXNG session key and
# the password on the Caddy login box in front of the app. Neither is ever
# printed. Vane ships no sign-in of its own, so that login box is the only thing
# between the internet and a setup screen holding your provider API key.
#
# NOT YET VERIFIED: no harness run has been recorded against this script.
set -euo pipefail

APP_DIR="${APP_DIR:-/srv/perplexica}"
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. ask.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 2048 ] || die "only ${avail_mb} MB of RAM available; Vane plus SearXNG wants 2048 MB"
avail_gb="$(df -BG --output=avail /srv | tail -1 | tr -dc '0-9')"
[ "$avail_gb" -ge 5 ] || die "only ${avail_gb} GB free on /srv; this install wants 5 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."

# basic_auth is what Caddy 2.8 renamed basicauth to, and the site block uses it.
caddy_line="$(caddy version | head -1)"
caddy_major="$(printf '%s' "$caddy_line" | sed -E 's/^v?([0-9]+)\.([0-9]+).*/\1/')"
caddy_minor="$(printf '%s' "$caddy_line" | sed -E 's/^v?([0-9]+)\.([0-9]+).*/\2/')"
if [ "$caddy_major" -lt 2 ] || { [ "$caddy_major" -eq 2 ] && [ "$caddy_minor" -lt 8 ]; }; then
	die "caddy 2.8 or newer is required for basic_auth; this box has ${caddy_line}"
fi

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

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

# SearXNG serves html only until told otherwise, and Vane asks for json.
sudo tee "$APP_DIR/searxng/settings.yml" >/dev/null <<'SETTINGS'
# SearXNG · the search backend here. Authored by caniselfhostit from
# https://docs.searxng.org/admin/settings/ and Vane's own install notes.
# No secret_key here: SEARXNG_SECRET in compose.yml overwrites it with the
# value install.sh generated. Nothing in this file is confidential.
use_default_settings: true

search:
  # SearXNG answers 403 to a format it was not told to serve, and ships html
  # only, so without json every Vane search fails.
  formats:
    - html
    - json

server:
  # The shipped default, stated so an upstream change cannot turn it on.
  limiter: false

engines:
  - name: wolframalpha
    disabled: false
SETTINGS
sudo chmod 644 "$APP_DIR/searxng/settings.yml"

# --- 3. Generate the two secrets, on the server ------------------------------
#
# Hex rather than base64 for both: one is typed into a browser dialog and the
# other is read by a container. Read the browser one later with
#   sudo cat /srv/perplexica/browser-login

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

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

# --- 4. The login box, then the Caddy site block, on the host ----------------
#
# Vane has no sign-in of its own, so Caddy is the sign-in. The hash is built
# from a file rather than an argument so the value never reaches the process
# list.

umask 077
caddy hash-password < "$APP_DIR/browser-login" > "$APP_DIR/vane-auth.hash"
printf 'basic_auth {\n\tvane %s\n}\n' "$(cat "$APP_DIR/vane-auth.hash")" > "$APP_DIR/vane-auth.conf"
umask 022
sudo install -m 640 -o root -g caddy "$APP_DIR/vane-auth.conf" /etc/caddy/vane-auth.conf
rm -f "$APP_DIR/vane-auth.hash" "$APP_DIR/vane-auth.conf"
[ "$(sudo grep -c basic_auth /etc/caddy/vane-auth.conf)" = "1" ] || die "/etc/caddy/vane-auth.conf did not get its basic_auth block"

if ! sudo grep -qF "$DOMAIN_HOST {" /etc/caddy/Caddyfile; then
	sudo cp /etc/caddy/Caddyfile "/etc/caddy/Caddyfile.before-perplexica"
	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. Ports: two open, and neither 8148 nor 8080 is one of them ------------

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

# --- 6. Start it -------------------------------------------------------------

docker compose pull
docker compose up -d

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

curl -sS "http://127.0.0.1:8148/api/config" | grep -q '"searxngURL":"http://searxng:8080"' \
	|| die "/api/config does not point at the searxng container. Check: docker compose logs --tail 40 vane"

curl -sS "http://127.0.0.1:8148/" | grep -q 'Welcome to' \
	|| die "the first screen is not the setup wizard. Check: docker compose logs --tail 40 vane"

# The search backend has to answer JSON, or every query fails later.
docker compose exec -T searxng wget -qO- 'http://127.0.0.1:8080/search?q=self+hosting&format=json' | grep -q '"query"' \
	|| die "searxng did not answer json. Check that $APP_DIR/searxng/settings.yml is mounted."

# Caddy must refuse an unauthenticated request. An open Vane on a public
# hostname is a stranger spending your money on your provider key.
unauth="$(curl -sS -o /dev/null -w '%{http_code}' "https://${DOMAIN_HOST}/" || true)"
[ "$unauth" = "401" ] || die "an unauthenticated request returned ${unauth}, not 401. Stop and investigate."

# --- 7. The first backup, before day one ends --------------------------------

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

cat <<-DONE

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

	  1. Sign in with the username  vane  and the password in
	     $APP_DIR/browser-login, mode 600. Read it with
	       sudo cat $APP_DIR/browser-login
	     and put it in your password manager. It was not printed here.
	     Vane has no sign-in of its own; that box is the whole login.
	  2. The first screen is a setup wizard. It asks for a model provider.
	     Paste your own API key from Anthropic, OpenAI or Google into it: the
	     answers are metered per token and billed to that account, and this
	     install holds no credential of its own.
	  3. Searches go through the SearXNG container, which asks public engines
	     and gets refused by some of them from a datacenter address. Thin
	     answers usually mean a suspended engine, not a broken install:
	       docker compose exec -T searxng wget -qO- 'http://127.0.0.1:8080/stats/errors'
	  4. First backup written to $APP_DIR/backups. It was taken before the
	     wizard, so your key is not in it yet: take another once setup is
	     done, and treat every backup after that like a password file,
	     because data/config.json holds the key. Either way it sits on the
	     same disk as the data, which is not a backup. Copy it somewhere
	     else tonight.

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 Perplexity.

  • The bill changes shape rather than disappearing. Vane writes nothing itself: every answer comes from an API key you hold at Anthropic, OpenAI or Google, metered per token and billed to you, with no ceiling. That key is stored in plain text in data/config.json, so treat that file and every backup of it like a password file.
  • You do not get the index. Perplexity crawls and maintains its own; SearXNG asks the public engines on your behalf, and those engines refuse requests from datacenter addresses. Google in particular blocks fresh instances within a handful of searches, and the honest consequence is thinner answers from fewer sources on a VPS than on your own home connection.
  • It ships with no sign-in at all, and upstream lists adding authentication as a future feature. This install puts a Caddy password box in front of it, because an open Vane on a public hostname is a stranger spending your money. That password is the whole login, and losing it means going back to the server.
  • This runs the slim image, which carries no headless browser. Quality mode still answers, from search-result snippets rather than by opening the pages, and the tool that summarises a URL you paste will not work. The full image that can do both also ships a SearXNG secret key shared by every copy of it, which is why this install does not use it.
  • The project renamed itself from Perplexica to Vane, and the repository, the Docker images and the documentation all moved. The old image name stopped receiving releases, so anything still pinned to it is frozen wherever it was.

Where this came from

“Make sure your SearxNG instance has: JSON format enabled in the settings, Wolfram Alpha search engine enabled”

  • Upstream publishes a slim image for people who already run SearXNG, and states that the instance it talks to needs JSON format and the Wolfram Alpha engine enabled. source
  • The full image bundles SearXNG with a settings file carrying one literal secret_key, and its start-up script launches SearXNG through sudo, which drops the SEARXNG_SECRET override. source
  • SearXNG returns 403 for any output format not listed under search.formats, and the shipped defaults list html only. source
  • The SEARXNG_SECRET environment variable overrides server.secret_key in settings.yml, which is how a generated key replaces the one in the file. source
  • Google actively blocks self-hosted SearXNG instances, and a fresh instance can be refused after a handful of searches. source

Questions people actually ask

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

  • Can I self-host Perplexity?

    Not Perplexity 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 Vane. An answering engine with citations, assembled from a SearXNG you run and a model key you already pay for. Renamed from Perplexica in 2026. The install is one evening: 2 containers 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 90 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 Perplexity?

    Vane. An answering engine with citations, assembled from a SearXNG you run and a model key you already pay for. Renamed from Perplexica in 2026. The only one here that reproduces the actual shape of the product: you ask a question, it searches, and the answer arrives with numbered citations under it, in an interface built for that and nothing else. What you take on is both halves of what Perplexity pays for. The index becomes a SearXNG asking public engines that increasingly refuse datacenter addresses, and the model becomes an API key metered to you. It is the honest trade, not a free lunch. Also evaluated and ranked below it: LibreChat. The prompt on this page installs Vane only. Vane is MIT-licensed and free; nothing on this page is a hosted service we sell you.

  • What does self-hosting cost compared to Perplexity?

    2048 MB of RAM and 5 GB of disk — the smallest tier most VPS hosts sell, about $10 a month. Vane itself is free and MIT-licensed; the bill is the server, plus a domain you probably already own. What you stop paying: Perplexity Pro, $20/mo — $240 a year.

  • How hard is it really?

    ONE EVENING — 1–3 hours. The rule that produced that verdict: up to three containers and at most one outside integration. You will type more than one command and read a page of documentation, and it will be running before you go to bed. The tier is derived from seven countable facts about the Vane install, not from anyone's impression of it, and the whole rubric is published on the methodology page.

  • Can I run Vane 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 Vane 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. The catch: Everything answers at http://localhost:8148, which is this computer and nowhere else, so your phone cannot open the answer you saved there and nothing searches while the lid is shut. 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.