Anubis with Docker: block AI scrapers and bots in front of your website (Linux, Windows, Mac)
Before you start
- 22 min read
Boxes marked Run in terminal are what you type; plain text explains why. Check what you need first

What you get in one minute#
Anubis is a free, self-hosted "web AI firewall": it sits in front of your website, asks anything that looks like a browser to solve a small proof-of-work puzzle once, lets search engines you trust walk through, and refuses scrapers that identify themselves as AI crawlers.

Logo: the official Anubis project mascot, drawn by CELPHASE and released under the repository's MIT licence (github.com/TecharoHQ/anubis)
It suits small sites, code forges, wikis and forums flooded by automated crawlers, with no CDN in front. The project calls it "a bit of a nuclear response" that can block harmless tools too, so this guide covers allow-lists as much as blocking. The README adds that if a CDN already protects your origin, you may not need Anubis.
| What | Value (official docs and release page, read November 2026) |
|---|---|
| Project status | Active and not renamed or archived. Latest release v1.28.2, published 23 October 2026 |
| Image to pin | ghcr.io/techarohq/anubis:v1.28.2 (tag checked on the registry; the docs' examples use latest) |
| Licence | MIT (LICENSE at the release tag) |
| Time needed | About 20 minutes for the Compose route (our estimate, not measured) |
| RAM | Not measured by us. The docs say 128Mi "may be sufficient" for many clients and "less than 32 MB on average" (author's estimates) |
| Ports | 8923 inside the container by default (the official Compose example uses 8080); 9090 for metrics. Neither is opened to the internet in this guide |
| Official docs | anubis.techaro.lol/docs and the repository |
At a glance:
- What it is: one small container between your reverse proxy and your app. A policy file allows, challenges or denies every request.
- What visitors see: a quick puzzle once, then a cookie that lasts a week. Known search crawlers are never asked.
- What it is not: a login, a server firewall or a perfect bot detector. The docs call the proof of work a "good enough placeholder".
- Platforms: the server runs on Linux. Windows and Mac can run the same Compose file in Docker Desktop for testing; there is no macOS release file.
- Privacy: the docs describe no telemetry. The challenge sets one cookie, which the docs say needs no EU cookie banner, though you may want to tell your visitors.
How it fits together#
Your reverse proxy sends every request to Anubis. Anubis decides, and only requests that pass reach your app.
Drawn for this guide from the official installation and Caddy pages. One Anubis instance protects one service.
Before you start#
You need a Linux server with Docker, a web app to protect, and (for HTTPS) a domain name.
| System | What to use |
|---|---|
| Linux (server) | Docker Engine and the Compose plugin, curl, openssl |
| Windows | Docker Desktop for testing. Anubis also ships a native Windows service (MSI), not covered here |
| Mac | Docker Desktop for testing. The release has no macOS file |
Check Docker on each system:
docker --version docker compose version
Checklist:
- [ ] Docker and Compose work (
docker compose version) - [ ] You know which service Anubis will protect and its port inside Docker
- [ ] Your app's own port is not published to the internet (Anubis cannot guard a door that stays open)
- [ ] For the HTTPS section only: a DNS record for the site, and ports 80 and 443 free
Tested by DIY Lab Hub: not run. The commands come from the official docs, the release files and the policy files at tag v1.28.2, read in November 2026. Expected outputs below are marked as derived from those files.
Step 1: Create the folders and the signing key#
Anubis signs the cookie it gives to visitors who pass. Without a key it makes a new one at every start, and the docs note this invalidates every outstanding challenge. The docs make a key with openssl rand -hex 32; we keep it in a private .env file.
Linux#
mkdir -p ~/docker_volumes/anubis/{data,config,logs,backups,www}
cd ~/docker_volumes/anubis
openssl rand -hex 32 | sed 's/^/ED25519_PRIVATE_KEY_HEX=/' > .env
chmod 600 .env
Windows (PowerShell)#
$base = "$HOME\docker_volumes\anubis"
foreach ($d in "data","config","logs","backups","www") { New-Item -ItemType Directory -Force "$base\$d" | Out-Null }
$bytes = New-Object byte[] 32
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
$rng.GetBytes($bytes)
$key = ($bytes | ForEach-Object { $_.ToString('x2') }) -join ''
"ED25519_PRIVATE_KEY_HEX=$key" | Set-Content -Encoding ascii "$base\.env"
Mac (Terminal)#
Same commands as Linux. macOS ships openssl, and Docker Desktop shares your home folder by default.
Check it worked. The file holds exactly one 64-character key. On Linux and Mac:
grep -c '^ED25519_PRIVATE_KEY_HEX=[0-9a-f]\{64\}$' ~/docker_volumes/anubis/.env
On Windows:
(Select-String -Path "$HOME\docker_volumes\anubis\.env" -Pattern '^ED25519_PRIVATE_KEY_HEX=[0-9a-f]{64}$').Count
Both print 1.
Tip: Never paste the key into a chat or a ticket. If it leaks, make a new one: the only effect is that visitors solve the puzzle again.
Step 2: Write the policy file and a demo page#
The first rule that matches a request wins, and a request that matches nothing is allowed. This file adds your own rules, imports the project's maintained default rules, and spells out the weight thresholds so you can see what each visitor costs.
The thresholds block is copied from the project's shipped botPolicies.yaml at v1.28.2. The custom rules are examples: 192.0.2.10 is a documentation address, so use your own monitor's address or delete that rule.
Linux#
cd ~/docker_volumes/anubis
echo '<h1>DIY Lab Hub demo page: you got through Anubis</h1>' > www/index.html
cat > config/botPolicy.yaml << 'EOF'
bots:
# Your own rules come first. The first match wins.
- name: demo-test-bot
user_agent_regex: DIYLabHubTestBot
action: DENY
- name: my-uptime-checker
action: ALLOW
remote_addresses:
- 192.0.2.10/32
- name: feeds
path_regex: ^/(feed|rss)(\.xml)?$
action: ALLOW
# The project's default rules: known bad bots, AI crawlers, good search engines.
- import: (data)/meta/default-config.yaml
status_codes:
CHALLENGE: 200
DENY: 200
thresholds:
- name: minimal-suspicion
expression: weight <= 0
action: ALLOW
- name: mild-suspicion
expression:
all:
- weight > 0
- weight < 10
action: CHALLENGE
challenge:
algorithm: metarefresh
difficulty: 1
- name: moderate-suspicion
expression:
all:
- weight >= 10
- weight < 20
action: CHALLENGE
challenge:
algorithm: fast
difficulty: 2
- name: mild-proof-of-work
expression:
all:
- weight >= 20
- weight < 30
action: CHALLENGE
challenge:
algorithm: fast
difficulty: 4
- name: extreme-suspicion
expression: weight >= 30
action: CHALLENGE
challenge:
algorithm: fast
difficulty: 6
EOF
Windows (PowerShell)#
$base = "$HOME\docker_volumes\anubis"
'<h1>DIY Lab Hub demo page: you got through Anubis</h1>' | Set-Content -Encoding ascii "$base\www\index.html"
@'
bots:
# Your own rules come first. The first match wins.
- name: demo-test-bot
user_agent_regex: DIYLabHubTestBot
action: DENY
- name: my-uptime-checker
action: ALLOW
remote_addresses:
- 192.0.2.10/32
- name: feeds
path_regex: ^/(feed|rss)(\.xml)?$
action: ALLOW
# The project's default rules: known bad bots, AI crawlers, good search engines.
- import: (data)/meta/default-config.yaml
status_codes:
CHALLENGE: 200
DENY: 200
thresholds:
- name: minimal-suspicion
expression: weight <= 0
action: ALLOW
- name: mild-suspicion
expression:
all:
- weight > 0
- weight < 10
action: CHALLENGE
challenge:
algorithm: metarefresh
difficulty: 1
- name: moderate-suspicion
expression:
all:
- weight >= 10
- weight < 20
action: CHALLENGE
challenge:
algorithm: fast
difficulty: 2
- name: mild-proof-of-work
expression:
all:
- weight >= 20
- weight < 30
action: CHALLENGE
challenge:
algorithm: fast
difficulty: 4
- name: extreme-suspicion
expression: weight >= 30
action: CHALLENGE
challenge:
algorithm: fast
difficulty: 6
'@ | Set-Content -Encoding ascii "$base\config\botPolicy.yaml"
Mac (Terminal)#
Same commands as Linux.
Check it worked. Both files exist: ls ~/docker_volumes/anubis/config ~/docker_volumes/anubis/www (Windows: Get-ChildItem $HOME\docker_volumes\anubis\config, $HOME\docker_volumes\anubis\www). A YAML mistake shows up in Step 3 as an error at start-up, not here.
Tip: Keep your own rules above the
importline. Rules are read top to bottom, and an allow rule below a deny rule never gets a chance.
Step 3: Write the Compose file and start Anubis#
This follows the official Docker Compose example (port 8080, POLICY_FNAME under /data/cfg, the anubis --healthcheck check) with the image pinned, the policy folder read-only, and the port published on 127.0.0.1 only. The demo nginx stands in for your app; the official example also uses an unpinned nginx, so replace it with your real service.
Linux#
cd ~/docker_volumes/anubis
cat > docker-compose.yml << 'EOF'
services:
anubis:
image: ghcr.io/techarohq/anubis:v1.28.2
container_name: anubis
restart: unless-stopped
env_file: .env
environment:
BIND: ":8080"
TARGET: "http://app:80"
POLICY_FNAME: "/data/cfg/botPolicy.yaml"
healthcheck:
test: ["CMD", "anubis", "--healthcheck"]
interval: 5s
timeout: 30s
retries: 5
start_period: 500ms
ports:
- "127.0.0.1:8080:8080"
volumes:
- ./config:/data/cfg:ro
app:
image: nginx
restart: unless-stopped
volumes:
- ./www:/usr/share/nginx/html:ro
EOF
docker compose up -d
docker compose ps
docker compose logs --tail=50 anubis
Windows (PowerShell)#
cd "$HOME\docker_volumes\anubis"
@'
services:
anubis:
image: ghcr.io/techarohq/anubis:v1.28.2
container_name: anubis
restart: unless-stopped
env_file: .env
environment:
BIND: ":8080"
TARGET: "http://app:80"
POLICY_FNAME: "/data/cfg/botPolicy.yaml"
healthcheck:
test: ["CMD", "anubis", "--healthcheck"]
interval: 5s
timeout: 30s
retries: 5
start_period: 500ms
ports:
- "127.0.0.1:8080:8080"
volumes:
- ./config:/data/cfg:ro
app:
image: nginx
restart: unless-stopped
volumes:
- ./www:/usr/share/nginx/html:ro
'@ | Set-Content -Encoding ascii docker-compose.yml
docker compose up -d
docker compose ps
docker compose logs --tail=50 anubis
Mac (Terminal)#
Same commands as Linux.
Check it worked. docker compose ps lists anubis and app as running, and anubis turns healthy within seconds. The docs publish no sample log output. A config error in the log means fixing the policy file (see Troubleshooting).
Tip: After every policy change run
docker compose restart anubisand readdocker compose logs --tail=50 anubis. Anubis refuses to start on a bad policy and prints the reason.
Step 4: Test the three outcomes#
Anubis reads the User-Agent header, so curl with different names shows each outcome without a browser. The expected results below follow from the policy files at v1.28.2, not from a recorded run.
Linux#
# 1. A plain client (curl's own name has no "Mozilla"): straight through to the app curl -s http://127.0.0.1:8080/ # 2. Something that claims to be a browser: gets the challenge page (assets under /.within.website/) curl -s -A "Mozilla/5.0" http://127.0.0.1:8080/ | grep -c "within.website" # 3. Your own deny rule and the shipped AI list: the demo page must NOT appear curl -s -A "DIYLabHubTestBot/1.0" http://127.0.0.1:8080/ | grep -c "demo page" curl -s -A "Amazonbot" http://127.0.0.1:8080/ | grep -c "demo page"
Windows (PowerShell)#
Use curl.exe: plain curl is an alias for another command in Windows PowerShell 5.1.
curl.exe -s http://127.0.0.1:8080/ curl.exe -s -A "Mozilla/5.0" http://127.0.0.1:8080/ | Select-String -Pattern "within.website" -SimpleMatch -Quiet curl.exe -s -A "DIYLabHubTestBot/1.0" http://127.0.0.1:8080/ | Select-String -Pattern "demo page" -SimpleMatch -Quiet curl.exe -s -A "Amazonbot" http://127.0.0.1:8080/ | Select-String -Pattern "demo page" -SimpleMatch -Quiet
Mac (Terminal)#
Same commands as Linux.
Check it worked. Expected (derived from the policy files):
<h1>DIY Lab Hub demo page: you got through Anubis</h1> (a number above 0) <- the challenge page; Windows prints True 0 <- denied; Windows prints False 0 <- denied; Windows prints False
Then open http://localhost:8080 in a normal browser. You should see the challenge page, and after a moment the demo page. Reload: you stay in, because the cookie is set.
Tip: A denied request still returns HTTP status 200 by default. The project does this on purpose, because the most aggressive scrapers stop retrying once they see a 200. Judge the result by the page body, not the status code.
Put it behind HTTPS (with or without Traefik)#
For a real site, the proxy in front of Anubis must serve HTTPS. This is for a server with a domain; local testers can skip it. In both routes Anubis joins a shared Docker network called proxy and stops publishing its port, so the only way in is through the proxy and the only way to your app is through Anubis. Create the network once:
docker network create proxy
Edit ~/docker_volumes/anubis/docker-compose.yml: remove the ports: lines of anubis, and add the networks. Your app stays on the default network only, so nothing can reach it around Anubis:
services:
anubis:
image: ghcr.io/techarohq/anubis:v1.28.2
container_name: anubis
restart: unless-stopped
env_file: .env
environment:
BIND: ":8080"
TARGET: "http://app:80"
POLICY_FNAME: "/data/cfg/botPolicy.yaml"
healthcheck:
test: ["CMD", "anubis", "--healthcheck"]
interval: 5s
timeout: 30s
retries: 5
start_period: 500ms
volumes:
- ./config:/data/cfg:ro
networks:
- default
- proxy
app:
image: nginx
restart: unless-stopped
volumes:
- ./www:/usr/share/nginx/html:ro
networks:
proxy:
external: true
With Traefik#
On a server that already runs Traefik with the Docker provider, add labels to the anubis service (not to your app). This assumes an entrypoint called https; the official Traefik page names it websecure, so use yours.
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.routers.anubis-app.rule=Host(`app.<your-domain>`)"
- "traefik.http.routers.anubis-app.entrypoints=https"
- "traefik.http.routers.anubis-app.tls=true"
- "traefik.http.services.anubis-app.loadbalancer.server.port=8080"
Then run docker compose up -d. Traefik sends the whole site to Anubis, and Anubis forwards what passes to TARGET.
The official Traefik page shows a second pattern: a forwardauth middleware that asks Anubis (http://anubis:8080/.within.website/x/cmd/anubis/api/check) before routing to the app. It needs TARGET set to one space, a separate anubis.<your-domain> host, and PUBLIC_URL, COOKIE_DOMAIN and REDIRECT_DOMAINS. Traefik reads any 2XX answer as "allowed", so set DENY: 403 in status_codes or denied bots walk through. The pattern above is simpler; forward-auth has open reports with several root domains (issue 1985) and copied examples that never challenge (issue 1578).
Check it worked. curl -I https://app.<your-domain>/ returns an HTTP status line over HTTPS, and a browser shows the challenge and then your site with a valid certificate.
Without Traefik: Caddy#
Choose this if you have no Traefik. Caddy gets and renews its own certificates. This is the official Caddy layout: Caddy talks to Anubis, Anubis talks to the app. Caddy's docs describe automatic Let's Encrypt certificates for a public domain, and tls internal for a local certificate at home.
mkdir -p ~/docker_volumes/caddy/{conf,data,config}
cd ~/docker_volumes/caddy
Save as ~/docker_volumes/caddy/docker-compose.yml. The image tag 2.11.8 exists on Docker Hub; Caddy 2.11.9 was released but had no Docker tag yet when we checked. The Caddyfile lives in the conf folder, which is mounted as a folder (Caddy's Docker docs warn against mounting a single file):
services:
caddy:
image: caddy:2.11.8
container_name: caddy
restart: unless-stopped
ports:
- "80:80/tcp"
- "443:443/tcp"
- "443:443/udp"
volumes:
- ./conf:/etc/caddy
- ./data:/data
- ./config:/config
networks:
- proxy
networks:
proxy:
external: true
Save as ~/docker_volumes/caddy/conf/Caddyfile for a public domain. The three header_up lines are from the official Anubis Caddy page:
app.example.com {
reverse_proxy anubis:8080 {
header_up X-Real-IP {remote_host}
header_up X-Http-Version {http.request.proto}
header_up X-TLS-SNI-Name {tls_server_name}
}
}
For a home-only name use tls internal, and trust Caddy's own root certificate on each device (our Caddy guide shows how on Linux, Windows and Mac):
app.home.arpa {
tls internal
reverse_proxy anubis:8080 {
header_up X-Real-IP {remote_host}
header_up X-Http-Version {http.request.proto}
header_up X-TLS-SNI-Name {tls_server_name}
}
}
Start both stacks:
cd ~/docker_volumes/caddy && docker compose up -d cd ~/docker_volumes/anubis && docker compose up -d
Windows and Mac are for testing only: run these on the Linux server.
Check it worked. curl -I https://app.example.com/ returns an HTTP status line over HTTPS. For a home-only name the browser shows a padlock only after the root is trusted on that device.
Tip: Let's Encrypt needs the DNS record to point at the server and ports 80 and 443 to reach it. A certificate error usually means the Caddyfile name differs from the DNS record.
Security and testing#
Anubis reduces load from lazy scrapers; it does not secure your app. These controls matter most.
| Control | How |
|---|---|
| Only one way in | Do not publish your app's port. If attackers can reach the app directly, Anubis is bypassed |
| Signing key | .env readable only by you (chmod 600); never in a shared folder or a Git repository |
| Metrics and debug | Metrics listen on :9090 inside the container and are not published here. The pprof debug routes are off by default; leave them off |
| Redirects | Set REDIRECT_DOMAINS to your own domains so a crafted challenge link cannot send visitors elsewhere |
| Client IPs | Anubis reads X-Forwarded-For. A public proxy in front (such as a CDN) hides the real IP and can cause blocks or loops; use CUSTOM_REAL_IP_HEADER as the docs describe |
| Cookies | COOKIE_SECURE defaults to true. Over plain HTTP outside localhost set it to false |
| Bypass by claiming to be Google | The shipped rules pair names with published IP ranges. Copy that pattern for your own allow rules |
Quick tests after each change:
cd ~/docker_volumes/anubis
docker compose ps
docker compose logs --tail=100 anubis
docker inspect --format '{{.State.Health.Status}}' anubis
The docs describe no telemetry, and the default policy needs no outside service. GeoIP rules need a database file you supply.
What the proof of work costs your visitors#
Anubis sends a challenge string, and the browser's JavaScript hashes it plus a counter with SHA-256 until the hash starts with enough zeros. The server checks the answer with one hash and sets a signed cookie, valid 168 hours (one week) by default per browser and site.
How hard the puzzle is depends on the request's "weight". In the shipped policy a browser-like request gets +10 weight, so a normal visitor falls into the moderate-suspicion row:
| Weight | Challenge | Difficulty | Average hashes (our arithmetic) |
|---|---|---|---|
| 0 or less | None (allowed) | - | 0 |
| 1 to 9 | metarefresh, no JavaScript, waits 1 second | 1 | none |
| 10 to 19 (a normal browser) | fast proof of work | 2 | about 256 |
| 20 to 29 | fast | 4 | about 65,000 |
| 30 or more | fast | 6 | about 16.8 million |
The hash counts are our arithmetic, not a benchmark: each extra zero multiplies the work by 16. The docs call difficulty 2 "very fast for most clients" and give no timings; we ran no tests.
Who still has trouble: visitors with JavaScript off, very old browsers (the docs list Chrome 75, Firefox 120 and Safari 10 as the minimum), browsers with the JIT disabled (slow), and extensions such as JShelter that block web workers (the docs give a workaround). Issue 1082 (closed) reports an iPhone X on iOS 14.2 stuck at 0 kh/s. The docs say the proof of work is a placeholder while better browser checks are built.
Let the good bots in#
The default rules let named search engines through only when the request also comes from the engine's published IP addresses, so a scraper cannot just write "Googlebot" in its user agent. Your tools:
| Goal | Rule or import |
|---|---|
| Google, Apple, Bing, DuckDuckGo, Qwant, Internet Archive, Kagi, Marginalia, Mojeek, Yandex and others | Already allowed by (data)/meta/default-config.yaml |
robots.txt, /.well-known, favicons, sitemap.xml | Already allowed (keep-internet-working.yaml) |
| Your uptime monitor or office network | Rule with remote_addresses (Step 2) |
| Another crawler you trust | User agent plus its published IP ranges (below) |
| RSS readers | A path_regex rule like feeds in Step 2 |
| Docker or other registry clients | - import: (data)/clients/docker-client.yaml |
| Git clients over HTTPS | - import: (data)/clients/git.yaml (see the open report in Troubleshooting) |
| Dillo, NetSurf, Pale Moon | - import: (data)/clients/small-internet-browsers/*.yaml (from the official FAQ) |
| AI assistants that fetch a page for a user | Denied by default (clients/ai.yaml). The default file suggests the ai-block-moderate or ai-block-permissive imports for a softer policy |
The pattern for another bot, from the official policy page (the address range is the example used there):
- name: qwantbot user_agent_regex: \+https\://help\.qwant\.com/bot/ action: ALLOW remote_addresses: ["91.242.162.0/24"]
Put new rules above the import line and restart Anubis. A deny-by-default setup (a catch-all DENY as the last rule) is possible, but it locks out everything you forgot.
Tip: Run with
SLOG_LEVEL: "DEBUG"for a day to see every request and decision, then switch back toINFO. It is the quickest way to find the good client you forgot to allow.
Common mistakes#
These come from the official docs' warnings, the project's issue tracker, and common AI assistant answers.
| Mistake | Why it happens | Do this instead |
|---|---|---|
| Publishing your app's port next to Anubis | The old ports: line stays in the Compose file. Anyone can skip Anubis | Remove the app's ports: and put it on a network only Anubis shares |
Using DENY with a status of 200 in forward-auth mode | The default is 200, which nginx (and Traefik for any 2XX) reads as "allowed" | Set status_codes: DENY: 403 when you use subrequest or forward-auth mode (official subrequest page) |
| Setting no signing key | Anubis makes a new key at every start | Set ED25519_PRIVATE_KEY_HEX in .env (Step 1); the key is also required for the bbolt store |
| Blocking Docker or Git clients | Browser-like user agents get challenged. Issue 1252 (closed): registry pulls fail. Issue 2068 (open): git clone fails on 1.28 | Import docker-client.yaml or git.yaml, or write an ALLOW rule |
Two Anubis instances on one domain, COOKIE_DOMAIN set | Cookies collide and the browser loops (issue 172, open) | One instance per hostname; set COOKIE_DOMAIN only for forward-auth |
| Putting a public proxy in front without fixing the IP header | Anubis keeps the right-most public address in X-Forwarded-For, which is then the proxy's | Follow the docs' client IP page: CUSTOM_REAL_IP_HEADER or clean the header first |
Using BASE_PREFIX behind Traefik | Issue 1712 (open) reports endless challenges | Give Anubis its own hostname instead of a path prefix |
| Binding a Unix socket in Docker | The image runs as user 1000, which cannot write to most folders (issue 2011, open) | Use the TCP port, or a folder owned by user 1000 |
AI assistants: inventing settings such as ANUBIS_DIFFICULTY or a Docker Hub image | Prefixed names look plausible, but the official table has none and the image is on ghcr.io | Use only names from the official environment-variable table and ghcr.io/techarohq/anubis:v1.28.2 |
| AI assistants: letting an agent file your bug report | The maintainer closed one agent-filed report (issue 1946): the repository does not accept them | Write it yourself, with version, policy file and log lines |
Tip: Before you copy a Traefik or nginx example, check which mode it uses. Reverse-proxy mode needs a
TARGET; forward-auth mode needsTARGETset to a single space.
Tip: Test with
curl -Abefore you change anything for real visitors (Step 4). If a tool you rely on is blocked, add an allow rule first and a block rule second.
Tip: Do not trust a copied policy file you have not read. Rules are tried in order, and one early
ALLOWwith a loose regex lets every bot through.
Troubleshooting#
| Problem | Solution |
|---|---|
| Anubis exits at start | docker compose logs --tail=100 anubis prints the policy error. Fix the YAML, then docker compose up -d |
| Endless challenge loop | Over plain HTTP outside localhost set COOKIE_SECURE: "false". Behind a proxy check the IP header. Try a private window with extensions off |
Redirect domain not allowed | Add your domain (and port, if not standard) to REDIRECT_DOMAINS |
config is not valid: ... you must set either bot rules or an import statement, not both | One rule has both import and its own fields. Split it into two list entries |
glob pattern matched no files | The import pattern matches nothing. Check the name; end it with .yaml |
| Everything blocked behind a CDN or public proxy | The proxy's address replaced the visitor's. Set CUSTOM_REAL_IP_HEADER |
redir=null | PUBLIC_URL is set while Anubis serves traffic directly. Leave it unset unless you use forward-auth |
| Search engine gets 403 or a challenge | Its name or IP is not in the allow rules. Read the log, then add a rule |
Your browser is configured to disable cookies#
The challenge page shows this when the verification cookie did not stick (issue 1584, closed; 1.28.0 added one retry, issue 1916). Check HTTPS or COOKIE_SECURE: "false", that the browser accepts first-party cookies, and that no second Anubis or proxy rewrites them.
auth request unexpected status: 400 while sending to client#
An nginx auth_request error on multipart POST uploads (issues 1995, 2062). The v1.28.2 notes say it is fixed; use 1.28.2 or later in subrequest mode.
Known bugs (open, November 2026)#
- Issue 2068:
git cloneover HTTPS is challenged on 1.28.x. The reporter's workaround is anALLOWrule for git user agents and theinfo/refsandgit-upload-packpaths; the shippedgit.yamldid not match their clients. - Issues 1985, 1712, 2011, 1916: forward-auth domains,
BASE_PREFIX, Unix sockets, and cookies randomly rejected (see above).
Run docker compose ps, docker compose logs --tail=200 anubis and docker inspect anubis first.
Updates and backups#
Update. Read the changelog first: the 1.28 line added WebAssembly challenges (off by default) and removed the Thoth service. Change the tag in docker-compose.yml, then:
cd ~/docker_volumes/anubis docker compose pull docker compose up -d docker compose logs --tail=100 anubis
Back up the policy and the key. They are small and secret, so keep the archive private:
cd ~/docker_volumes/anubis mkdir -p backups tar -czf "backups/anubis-$(date +%F).tar.gz" config .env docker-compose.yml chmod 600 backups/anubis-*.tar.gz
On Linux and Mac, run this nightly at 03:00 from crontab, and copy the archive to another machine.
Windows (PowerShell)#
cd "$HOME\docker_volumes\anubis" New-Item -ItemType Directory -Force backups | Out-Null tar -czf "backups\anubis-$(Get-Date -Format yyyy-MM-dd).tar.gz" config .env docker-compose.yml
Windows 10 and 11 include tar. Schedule this with Task Scheduler (daily, 03:00).
Mac (Terminal)#
Same commands as Linux. Docker Desktop runs only while it is open.
Restore test. Unpack into a new empty folder, never over the live one, and check that the files are there:
mkdir -p /tmp/restore-test && tar -xzf backups/anubis-<date>.tar.gz -C /tmp/restore-test ls -a /tmp/restore-test
Performance and resources#
The docs give estimates, not measurements, and we measured nothing. Use these to find out for yourself:
- Resources:
docker stats anubisshows live CPU and memory. - Health:
docker compose psshows the built-inanubis --healthcheckresult. - Metrics: Anubis serves Prometheus metrics on
:9090inside the container. Publish it as127.0.0.1:9090:9090only if you run a monitor on the same host. - Challenge store: the default is in memory, which the docs say is for testing and small setups. For a busy single-host site, the docs recommend
bbolt. Addstore:withbackend: bboltandparameters: path: /data/db/anubis.bdbto the policy file, mount./data:/data/db, and make the folder writable for user and group 1000 (sudo chown 1000:1000 data). The signing key from Step 1 is required for this store. - Several sites: one Anubis instance per protected service, each with its own container name.
FAQ#
Is Anubis free to use? Yes, under the MIT licence. Techaro also offers BotStopper, whose extra settings the docs mark enterprise-only; this guide does not need it.
Does it need a GPU? No. The challenge runs in the visitor's browser, and the server only checks one hash.
Does it mine crypto? No. The official FAQ says Anubis does not mine Bitcoin or any other cryptocurrency.
Does Anubis stop every scraper? No. A scraper that runs a full browser and pays the cost can still get through. It raises the price of mass crawling and blocks the lazy ones.
Will it hurt my search ranking? The default rules allow the main search crawlers when their IP address matches. Check your search console for challenge or 403 pages after you add rules of your own.
Sources#
- Anubis release v1.28.2, README and LICENSE - read November 2026
- Docs at the tag: admin pages (installation, policies, default allow behaviour, thresholds, importing rules, redirect domains, subrequest authentication, client IP headers, FAQ, and the Docker Compose, Caddy, Traefik and Windows environments), design pages, user pages and the changelog, also rendered at anubis.techaro.lol/docs - read November 2026
- Policy files at the tag: default policy, default-config, good crawlers and AI catch-all - read November 2026
- Anubis image on GitHub Container Registry (tag
v1.28.2checked) - November 2026 - Caddy release v2.11.9, Caddy Docker image (tag
2.11.8checked), Caddy automatic HTTPS and Traefik forwardAuth - read November 2026 - GitHub issues #172, #1082, #1252, #1578, #1584, #1712, #1916, #1946, #1985, #1995, #2011, #2062 and #2068 - read November 2026
- Related guides: Caddy, CrowdSec, BunkerWeb and Forgejo
Checked against the official docs on 2026-11-05.