Run SearXNG With Docker Compose Behind Caddy or Nginx

Start from SearXNG's official Compose setup instead of a random blog paste, then layer a reverse proxy on top so the search instance is reachable without turning it into a public abuse magnet by accident.

SearXNG Docker Compose Reverse proxy
Illustrated guide cover for Run SearXNG With Docker Compose Behind Caddy or Nginx
SearXNG • Docker Compose • Reverse proxy
What you learn

How to bootstrap the official SearXNG Compose environment, edit .env and core-config/settings.yml, and proxy it with TLS.

Best for

Self-hosters who want a privacy-respecting metasearch instance and are willing to treat public exposure, rate limits, and maintenance seriously.

Risk to watch

A public search endpoint attracts abuse fast. If you skip proxy, bot-limiting, and configuration hygiene, the project becomes work instead of a tool.

Before you begin

  • A host with Docker or Podman. This guide assumes Docker Compose.
  • A domain or subdomain you control.
  • A plan to keep the service private or at least rate-limited and monitored if you expose it publicly.
  • Patience for reading the official config comments instead of skipping straight to a random default.

The SearXNG documentation still recommends Compose instancing as the preferred container deployment model. The official flow creates a core-config directory, fetches the upstream docker-compose.yml and .env.example, then asks you to copy .env.example to .env and configure core-config/settings.yml.

Expected outcome: you will finish with a working SearXNG stack listening locally on the Docker host and a reverse proxy handling the public URL and HTTPS.

Step 1: Bootstrap the official Compose environment

Create the app directory and fetch the current templates:

mkdir -p /opt/stacks/searxng/core-config
cd /opt/stacks/searxng
curl -fsSL \
  -O https://raw.githubusercontent.com/searxng/searxng/master/container/docker-compose.yml \
  -O https://raw.githubusercontent.com/searxng/searxng/master/container/.env.example
cp -i .env.example .env

Starting from the official templates lowers the chance that you miss required services or drift onto stale container names. It also makes future upstream updates easier to compare.

Step 2: Configure SearXNG sanely

Edit .env first. The exact variable set may change upstream, so keep the file close to the version you fetched rather than copying an old snippet forever.

Then create or update core-config/settings.yml. SearXNG's own docs call out server.base_url, a real secret_key, and limiter settings as the critical pieces for a public instance. A safer starting point looks like this:

use_default_settings: true

server:
  base_url: https://search.example.com/
  secret_key: change-this-to-a-long-random-value
  limiter: true
  public_instance: true

search:
  safe_search: 1

ui:
  static_use_hash: true

If you are only exposing SearXNG through a private tunnel such as Tailscale or SSH port forwarding, keep the service private-first and decide separately whether the extra public-instance behavior is useful for your case.

For a public deployment, also create core-config/limiter.toml so the limiter trusts your reverse proxy and enables a stronger bot-abuse posture:

[botdetection]
trusted_proxies = [
  '127.0.0.0/8',
  '::1',
]

[botdetection.ip_limit]
link_token = true

Keep the rest of the configuration deliberately small until the service is working. The project's config surface is powerful, which means it is easy to introduce three variables worth of confusion before you even get a homepage.

Warning: if you plan to expose the instance publicly, treat limiter behavior, trusted proxy settings, and reverse-proxy headers as mandatory, not optional hardening polish. Private-only access is the lower-friction option if you do not actually need a public search endpoint.

Step 3: Start the services and validate locally

Bring the stack up:

docker compose up -d
docker compose ps

Check the main service logs:

docker compose logs -f core

The official docs show the core service typically bound on host port 8080. Test it locally before adding the public proxy:

curl -I http://127.0.0.1:8080

If the local response fails, fix that first. A reverse proxy will not rescue a broken base app.

Step 4: Put Caddy or Nginx in front

A minimal Caddy entry:

search.example.com {
  reverse_proxy 127.0.0.1:8080
}

A minimal Nginx server block:

server {
  listen 80;
  server_name search.example.com;

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

Nginx needs explicit header forwarding more often than Caddy does. If the base URL or forwarded protocol is wrong, login or preference changes can misbehave, and if X-Forwarded-For or X-Real-IP is wrong the limiter cannot identify client IPs correctly.

Step 5: Operate and update the instance

Routine checks:

docker compose ps
docker compose logs --tail 50 core
docker compose logs --tail 50 valkey

When updating templates or services, follow the upstream pattern:

docker compose down
curl -fsSL \
  -O https://raw.githubusercontent.com/searxng/searxng/master/container/docker-compose.yml \
  -O https://raw.githubusercontent.com/searxng/searxng/master/container/.env.example
docker compose pull
docker compose up -d

Review template changes before restarting production. SearXNG evolves quickly enough that assuming an old .env still matches the new compose layout is careless.

Rollback and backup notes

Preserve the configuration under core-config/ and the service data volumes. The official docs identify configuration under /etc/searxng and cache data under /var/cache/searxng as the persistent pieces that matter.

If a template refresh breaks the deployment, restore the prior compose file and .env, then bring the services back up with the last known-good image set.

Troubleshooting

The reverse proxy works but searches fail oddly.
Recheck server.base_url, the forwarded host and proto headers, and whether trusted_proxies, X-Forwarded-For, and X-Real-IP reflect your real proxy path.

The stack starts but the homepage is blank or broken.
Validate the local port on 127.0.0.1:8080 first and inspect docker compose logs -f core.

The instance gets hammered after going public.
That is not surprising. Enable the limiter, confirm the reverse proxy is passing real client IP headers, tighten limiter.toml, or retreat to private-only exposure.

Updating broke the deployment.
Compare the refreshed template with the previous one. SearXNG's own docs warn that template changes can affect deployment behavior.