Anubis 1.28 Docker Compose 部署实战

Docker Compose 编排部署 Anubis v1.28.2 反 AI 爬虫质询代理的完整实战:官方镜像选型、策略文件挂载、直连后端与经 Anubis 质询的全链路对照实测、浏览器模拟 PoW 解题换 Cookie、metrics 与日志核验,以及 X-Real-IP 缺失与 HTTP 循环质询两个排障实录。

Anubis安全爬虫WAFGolang2026/10/28

Anubis 1.28 Docker Compose 部署实战

目标:在一台 Linux 服务器上以 Docker Compose 编排部署 Anubis v1.28.2 反 AI 爬虫质询代理,并用一个静态站点作为被保护上游,完整演示「直连后端 vs 经 Anubis 质询」的防护链路:curl 模拟浏览器请求被下发 Proof-of-Work 质询、脚本完成 PoW 计算拿到通行 Cookie 后直达上游、Amazonbot 被 DENY 拒绝、metrics 指标与日志核验,以及两次真实排障实录(缺失 X-Real-IP 返回 500、HTTP 站点默认 Secure Cookie 导致循环质询)。全部命令与回显均已在 Ubuntu 24.04 服务器实测通过。选型依据见 《Anubis 介绍与选型指南》。

一、环境信息

  • 操作系统:Ubuntu 24.04.5 LTS(x86_64)
  • Docker Engine:29.9.2,Docker Compose:v5.6.1
  • Anubis:ghcr.io/techarohq/anubis:v1.28.2(官方 OCI 镜像,2026-10-28 查询的官方最新稳定版)
  • 被保护上游:nginx:alpine 提供的静态演示站
  • 端口规划(宿主侧):8133 → Anubis 质询入口(容器内 8923)、8135 → 上游静态站直连对照(容器内 80)、8136 → Prometheus 指标端点(容器内 9090)
  • 独立网络:anubis-net,显式子网 10.89.62.0/24
  • 工作目录:/opt/anubis/(compose 编排、策略文件、演示站点目录均存放于此)
  • 质询难度:DIFFICULTY=4(PoW 结果需满足 SHA-256 哈希前导 4 个零)

二、镜像选型说明

Anubis 官方仅发布 OCI 镜像于 ghcr.io/techarohq/anubis,tag 约定如下(官方 installation 文档原文约定,查询日期 2026-10-28):

Tag含义选型建议
latest最新稳定发布不确定时从这里开始
pre预发布版想提前体验即将转正的功能时使用
v<版本号>指定版本发布(如 v1.28.2)生产推荐,版本锁定可复现
mainmain 分支当前构建仅用于跟进未发版的新特性

本篇实测固定使用 v1.28.2:生产部署应锁定具体版本号而非 latest,保证策略行为与质询算法随版本可追溯。镜像以 UID 1000/GID 1000 运行(官方约定),若挂载外部卷需保证该属主可写——本篇策略文件以只读方式挂载,不受影响。

拉取镜像(输出实测,2026-10-28):

docker pull ghcr.io/techarohq/anubis:v1.28.2
3d4c4b73c020: Pull complete
Digest: sha256:5fcbce70ee6168be5b226d19bec90111d0acd0daf13028fa06b044cf3ebbbd5f
Status: Downloaded newer image for ghcr.io/techarohq/anubis:v1.28.2

验证本地镜像:

docker images | grep anubis
ghcr.io/techarohq/anubis    v1.28.2    5fcbce70ee61    71.2MB    18.3MB

单实例镜像体积约 71MB,官方文档注明 Anubis 运行期常驻内存通常低于 32MB,属极轻量的过滤层。

三、前置条件

  1. 服务器已安装 Docker Engine 29.x 与 Compose v2 插件(本篇实测版本见环境信息);
  2. 端口 8133、8135、8136 未被占用(用 ss -ltn | grep -E '8133|8135|8136' 核查,无输出即可用);
  3. 明确部署拓扑:Anubis 是过滤型反向代理,必须放在「TLS 终结层(Nginx/Caddy 等)」与「被保护业务」之间,一个 Anubis 实例保护一个上游服务。本篇为演示完整链路,用 Compose 同时编排被保护的静态站,生产中该上游通常是你已有的 GitLab/Gitea/论坛等独立服务;
  4. 已想好策略基线:本篇采用「最小演示策略」(放行 /.well-known/、favicon、robots.txt,DENY 掉 Amazonbot,对含 Mozilla/Opera 的 UA 一律下发质询)。

四、部署步骤

4.1 创建工作目录与演示站点

mkdir -p /opt/anubis/www
cd /opt/anubis

创建上游静态演示页 /opt/anubis/www/index.html:

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>Anubis 保护的后端演示站</title></head>
<body>
<h1>Hello from upstream backend</h1>
<p>This page is served by the upstream static site behind Anubis.</p>
</body>
</html>

4.2 编写质询策略文件

创建 /opt/anubis/botPolicy.yaml(官方 policies 文档的最小策略样例基础上,补充一条 AI 爬虫 DENY 规则):

bots:
  - name: deny-amazonbot
    user_agent_regex: Amazonbot
    action: DENY
  - name: well-known
    path_regex: ^/.well-known/.*$
    action: ALLOW
  - name: favicon
    path_regex: ^/favicon.ico$
    action: ALLOW
  - name: robots-txt
    path_regex: ^/robots.txt$
    action: ALLOW
  - name: generic-browser
    user_agent_regex: Mozilla|Opera
    action: CHALLENGE

规则自上而下依次匹配,命中即裁决:deny-amazonbot 把 UA 含 Amazonbot 的请求直接拒绝;well-known/favicon/robots-txt 三条放行「维持互联网运转」的基础路径;generic-browser 对所有像浏览器的 UA(含 Mozilla/Opera)下发质询。没有任何规则命中时请求默认放行——所以纯 curl/8.5.0 这样的 UA 不含 Mozilla,会直通上游(下文 4.5 实测可验证)。策略体系的完整参数见 《Anubis v1.28.2 配置参数详解》。

4.3 编写 Compose 编排

创建 /opt/anubis/compose.yaml(项目名显式声明为 anubis-1.28.2-stack,避免与其他栈混淆):

name: anubis-1.28.2-stack

services:
  anubis:
    image: ghcr.io/techarohq/anubis:v1.28.2
    container_name: anubis-1.28.2-server
    environment:
      BIND: ":8923"
      DIFFICULTY: "4"
      METRICS_BIND: ":9090"
      SERVE_ROBOTS_TXT: "true"
      TARGET: "http://anubis-1.28.2-backend"
      POLICY_FNAME: "/data/cfg/botPolicy.yaml"
      SLOG_LEVEL: "DEBUG"
      COOKIE_SECURE: "false"
      COOKIE_SAME_SITE: "Lax"
      WEBMASTER_EMAIL: "root@example.com"
    ports:
      - "8133:8923"
      - "8136:9090"
    volumes:
      - ./botPolicy.yaml:/data/cfg/botPolicy.yaml:ro
    healthcheck:
      test: ["CMD", "anubis", "--healthcheck"]
      interval: 5s
      timeout: 30s
      retries: 5
      start_period: 500ms
    networks:
      anubis-net:
        ipv4_address: 10.89.62.10
    depends_on:
      - backend

  backend:
    image: nginx:alpine
    container_name: anubis-1.28.2-backend
    volumes:
      - ./www:/usr/share/nginx/html:ro
    ports:
      - "8135:80"
    networks:
      anubis-net:
        ipv4_address: 10.89.62.20

networks:
  anubis-net:
    driver: bridge
    ipam:
      config:
        - subnet: 10.89.62.0/24

编排要点逐项说明:

  • TARGET 指向上游服务的网络内地址(http://anubis-1.28.2-backend,Compose 网络内按服务可达的容器名解析);生产中替换为你的业务服务地址,也支持 unix:///path/to/sock;
  • COOKIE_SECURE: "false" 与 COOKIE_SAME_SITE: "Lax":这两项是纯 HTTP 演示链路能走通的关键(官方默认 COOKIE_SECURE=true、COOKIE_SAME_SITE=None,均要求 HTTPS 上下文;HTTP 下保留默认值会陷入循环质询,排障实录见 6.2);
  • SLOG_LEVEL: "DEBUG":观测期开 DEBUG 便于在日志中看到每条请求的规则裁决与质询流转,稳定后建议回调 INFO;
  • healthcheck 用官方提供的 anubis --healthcheck 子命令做进程级健康自检;
  • 策略文件以 :ro 只读挂载进 /data/cfg/botPolicy.yaml,与 POLICY_FNAME 对应。

4.4 启动栈并验证健康状态

cd /opt/anubis && docker compose up -d
 Container anubis-1.28.2-backend Starting
 Container anubis-1.28.2-backend Started
 Container anubis-1.28.2-server Starting
 Container anubis-1.28.2-server Started

数秒后确认两服务均为 healthy/Up:

docker compose ps --format '{{.Name}}\t{{.Status}}'
anubis-1.28.2-backend	Up 8 seconds
anubis-1.28.2-server	Up 8 seconds (healthy)

再确认启动日志无 ERROR(输出节选,实测首尾):

docker compose logs anubis 2>&1 | tail -2
{"time":"2026-10-27T17:30:04.763293895Z","level":"DEBUG","source":{"function":"github.com/TecharoHQ/anubis/internal.RemoteXRealIP","file":"github.com/TecharoHQ/anubis/internal/headers.go","line":70},"msg":"skipping middleware, useRemoteAddress is empty"}
{"time":"2026-10-27T17:30:04.76317453Z","level":"INFO","source":{"function":"main.run","file":"github.com/TecharoHQ/anubis/cmd/anubis/main.go","line":487},"msg":"listening","url":"http://localhost:8923","difficulty":4,"serveRobotsTXT":true,"target":"http://anubis-1.28.2-backend","version":"v1.28.2","use-remote-address":false,"custom-real-ip-header":"","debug-benchmark-js":false,"og-passthrough":false,"og-expiry-time":86400000000000,"base-prefix":"","cookie-expiration-time":604800000000000,"rule-error-ids":{"deny-amazonbot":"d7f8451854c8a486"},"public-url":""}

listening 日志里 version/difficulty/target 与编排一致,启动即成功。

4.5 完整链路实测:直连后端 vs 经 Anubis 质询

对照一:直连上游(8135),不经过 Anubis,任何 UA 直接拿到真实页面:

curl -sS -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:8135/
HTTP 200
curl -s http://127.0.0.1:8135/ | head -3
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>Anubis 保护的后端演示站</title></head>

对照二:经 Anubis(8133)+ 非浏览器 UA,未命中质询规则,默认放行直达上游(需带 X-Real-IP,缺失该头的报错见排障实录 6.1):

curl -sS -o /dev/null -w "HTTP %{http_code} (direct pass-through)\n" \
  -H "X-Real-IP: 203.0.113.10" http://127.0.0.1:8133/
curl -s -H "X-Real-IP: 203.0.113.10" http://127.0.0.1:8133/ | grep -o "<h1>.*</h1>"
HTTP 200 (direct pass-through)
<h1>Hello from upstream backend</h1>

对照三:经 Anubis + 浏览器 UA,命中 generic-browser 规则被下发 PoW 质询。注意状态码是 200 而非 403——这是官方刻意设计(默认 status_codes: {CHALLENGE: 200, DENY: 200},让最激进的爬虫拿到 200 后停手):

curl -sS -o /dev/null -w "HTTP %{http_code}\n" \
  -A "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0" \
  -H "X-Real-IP: 203.0.113.10" http://127.0.0.1:8133/
curl -s -A "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0" \
  -H "X-Real-IP: 203.0.113.10" http://127.0.0.1:8133/ \
  | grep -oE "<title>[^<]*</title>"
HTTP 200
<title>Making sure you&#39;re not a bot!</title>

对照四:AI 爬虫 UA(Amazonbot),命中 deny-amazonbot 被拒绝,返回让爬虫误以为成功的错误页:

curl -sS -o /dev/null -w "HTTP %{http_code}\n" \
  -A "Amazonbot/1.0 (+https://developer.amazon.com/amazonbot)" \
  -H "X-Real-IP: 203.0.113.10" http://127.0.0.1:8133/
curl -s -A "Amazonbot/1.0" -H "X-Real-IP: 203.0.113.10" http://127.0.0.1:8133/ \
  | grep -oE "<title>[^<]*</title>" | head -1
HTTP 200
<title>Oh noes!</title>

四组对照即为防护链路的行为基线:真实浏览器 UA 被拦在质询页,AI 爬虫被静默拒绝,非浏览器 CLI 流量默认直通。

真实浏览器会在质询页内执行 JavaScript 计算 PoW 后自动回传。为验证全链路,用 Python 脚本复演浏览器行为:解析质询页内嵌的 challenge JSON → 暴力计算满足难度 4 前导零的 nonce → GET 跳转 pass-challenge 接口换取 Cookie → 重访上游。脚本保存为 /opt/anubis/pow_client.py:

import hashlib, json, re, time, urllib.parse, urllib.request

BASE = "http://127.0.0.1:8133"
UA = "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"

jar = {}

def req(path):
    r = urllib.request.Request(BASE + path)
    r.add_header("User-Agent", UA)
    r.add_header("X-Real-IP", "203.0.113.10")
    if jar:
        r.add_header("Cookie", "; ".join(f"{k}={v}" for k, v in jar.items()))
    class NoRedirect(urllib.request.HTTPRedirectHandler):
        def redirect_request(self, *a, **k): return None
    try:
        resp = urllib.request.build_opener(NoRedirect).open(r, timeout=30)
    except urllib.error.HTTPError as e:
        resp = e
    for h in resp.headers.get_all("Set-Cookie") or []:
        kv = h.split(";")[0]
        k, _, v = kv.partition("=")
        jar[k.strip()] = v.strip()
    return resp

# 第 1 步:请求受保护页面,拿到服务端渲染的质询页与内嵌 challenge JSON
resp = req("/")
html = resp.read().decode()
ch = json.loads(re.search(r'id="anubis_challenge" type="application/json">(.*?)</script>', html, re.S).group(1))
print("step1 challenge page ->", resp.status, "| challenge id:", ch["challenge"]["id"])
print("  method:", ch["challenge"]["method"], "| difficulty:", ch["challenge"]["difficulty"])

# 第 2 步:本地暴力计算(等价于浏览器内 sha256 worker 的循环)
random_data = ch["challenge"]["randomData"]
difficulty = ch["challenge"]["difficulty"]
start = time.time()
nonce = 0
while True:
    digest = hashlib.sha256((random_data + str(nonce)).encode()).hexdigest()
    if digest.startswith("0" * difficulty):
        break
    nonce += 1
cost = int((time.time() - start) * 1000)
print(f"step2 PoW solved: nonce={nonce} sha256={digest} elapsed={cost}ms")

# 第 3 步:GET 跳转 pass-challenge 回传 proof,换取通行 Cookie
qs = urllib.parse.urlencode({"id": ch["challenge"]["id"], "response": digest,
                             "nonce": str(nonce), "redir": "/", "elapsedTime": str(cost)})
resp = req("/.within.website/x/cmd/anubis/api/pass-challenge?" + qs)
print("step3 pass-challenge ->", resp.status, "| location:", resp.headers.get("Location"))
print("  cookies now:", list(jar.keys()))

# 第 4 步:持凭证重访受保护页面
resp = req("/")
body = resp.read().decode()
print("step4 revisit / ->", resp.status)
t = re.search(r"<h1>(.*?)</h1>", body)
print("  upstream title:", t.group(1) if t else body[:100])

执行与实测回显(2026-10-28):

python3 /opt/anubis/pow_client.py
step1 challenge page -> 200 | challenge id: 01a17e6b-e7bb-76fe-a2cb-95876e86f7de
  method: fast | difficulty: 4
step2 PoW solved: nonce=19767 sha256=00001302174bcfd0dd65a46bedbeaf8adc996a9667825e33a63dce0de554f1bb elapsed=9ms
step3 pass-challenge -> 302 | location: /
  cookies now: ['techaro.lol-anubis-cookie-verification-a9d36a45', 'techaro.lol-anubis-auth-a9d36a45', 'techaro.lol-anubis-original-referer-a9d36a45']
step4 revisit / -> 200
  upstream title: Hello from upstream backend

全链路证据链解读:

  • 质询页 200 返回,内嵌 anubis_challenge JSON:method: fast、difficulty: 4、randomData 为质询随机串;
  • 客户端对 randomData + nonce 逐次 SHA-256,第 19767 次得到前 4 位为零的哈希(00001302…),耗时 9ms——真实浏览器中这由质询页内置的 sha256 worker(WASM/多线程)完成,难度 4 对普通设备是一瞬间的开销;
  • 携带 id/nonce/response/redir GET 跳转 pass-challenge,Anubis 校验通过后 302 回 redir,同时签发 3 枚 Cookie(…-anubis-auth-… 为通行凭证,后缀 a9d36a45 由 DYNAMIC_COOKIE_SUFFIX 按当前 Cookie 配置自动生成,防止同名 Cookie 死循环);
  • 持凭证重访直接 200 拿到上游真实页面,且凭证默认 168 小时有效(COOKIE_EXPIRATION_TIME)。

4.7 metrics 与日志核验

访问指标端点,确认质询行为的量化痕迹(输出节选,实测):

curl -s http://127.0.0.1:8136/metrics | grep -E "^anubis_(challenges|policy_results)"
anubis_challenges_issued{asn="",asn_description="",method="embedded"} 3
anubis_challenges_validated{asn="",asn_description="",method="fast"} 1
anubis_policy_results{action="ALLOW",asn="",asn_description="",rule="default/allow"} 2
anubis_policy_results{action="CHALLENGE",asn="",asn_description="",rule="bot/generic-browser"} 4
anubis_policy_results{action="DENY",asn="",asn_description="",rule="bot/deny-amazonbot"} 2

指标与 4.5 的行为一一对应:embedded 计数是下发的质询页数,validated 是 PoW 通过数(4.6 完成的那次),policy_results 按「动作/规则」维度分桶计数。

再从日志侧核验质询通过记录(SLOG_LEVEL=DEBUG 下的关键行,实测):

docker compose logs anubis 2>&1 | grep "challenge accepted" | tail -1
{"time":"2026-10-27T17:32:43.344108865Z","level":"INFO","source":{"function":"github.com/TecharoHQ/anubis/lib.(*Server).PassChallenge","file":"github.com/TecharoHQ/anubis/lib/anubis.go","line":754},"msg":"challenge accepted","subsystem":"anubis","host":"127.0.0.1:8133","method":"GET","path":"/.within.website/x/cmd/anubis/api/pass-challenge","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0","accept_language":"","priority":"","x-forwarded-for":"","x-real-ip":"203.0.113.10","check_result":{"name":"bot/generic-browser","rule":"CHALLENGE","weight":0},"challenge":"01a17e6b-e7bb-76fe-a2cb-95876e86f7de"}

日志中的 check_result 字段直接给出命中的规则名(bot/generic-browser)、动作(CHALLENGE)与权重(0),是排查「某条请求为什么被拦/放行」的第一入口。

五、数据持久化说明

Anubis 自身不承载业务数据,需要持久化的只有两类:

  1. 策略文件:/opt/anubis/botPolicy.yaml,本篇以只读 bind mount 挂载,改动文件后 docker compose restart anubis 生效;
  2. 质询临时状态:默认 memory 存储后端,重启后已签发的通行 Cookie 全部失效(用户需重新过质询)。若在意重启体验或未来横向扩容,改用持久存储——在策略文件中追加 store 段(如 bbolt 单机落盘或 valkey 远端共享),详见 《Anubis v1.28.2 配置参数详解》;使用 bbolt 等持久后端时必须固定 ED25519_PRIVATE_KEY_HEX(否则每次启动随机生成密钥,历史 Cookie 全部作废),启动日志中的随机密钥告警即为提示:
{"level":"WARN","msg":"generating random key, Anubis will have strange behavior when multiple instances are behind the same load balancer target, for more information: see https://anubis.techaro.lol/docs/admin/installation#key-generation"}

上游静态站数据在 /opt/anubis/www/,同样随 bind mount 持久化。验证持久化路径:docker compose down && docker compose up -d 后,/opt/anubis/ 下三份文件均原地保留,重挂即恢复。

六、环境变量表

本篇编排用到的环境变量实测清单(默认值核对自官方 installation 文档,查询日期 2026-10-28):

环境变量本篇取值默认值作用
BIND:8923:8923Anubis 主监听地址(质询入口)
TARGEThttp://anubis-1.28.2-backendhttp://localhost:3923质询通过后转发的上游服务地址
POLICY_FNAME/data/cfg/botPolicy.yaml未设(用内置默认策略)策略文件路径
DIFFICULTY44PoW 难度(前导零个数)
METRICS_BIND:9090:9090Prometheus 指标服务监听地址
SERVE_ROBOTS_TXTtruefalse由 Anubis 直接下发「拒绝全部 AI 爬虫」的 robots.txt
COOKIE_SECUREfalsetrueCookie 是否带 Secure 标记;纯 HTTP 部署必须显式关掉,否则循环质询
COOKIE_SAME_SITELaxNoneCookie SameSite 属性;None 要求 HTTPS,HTTP 用 Lax
SLOG_LEVELDEBUGINFO结构化日志级别(DEBUG/INFO/WARN/ERROR)
WEBMASTER_EMAILroot@example.com未设错误页展示的站长联系方式,便于被误伤用户申诉

更完整的变量(COOKIE_DOMAIN、COOKIE_EXPIRATION_TIME、ED25519_PRIVATE_KEY_HEX、BASE_PREFIX、TARGET_HOST 等三十余项)逐项解析见 《Anubis v1.28.2 配置参数详解》。

七、配置参数说明

围绕本篇编排出现的关键配置,给出选择依据:

  • DIFFICULTY: "4":官方默认值。每加 1,PoW 计算量约乘 16(前导零概率 2^-4 → 2^-5);真实用户浏览器 4≈几十毫秒、6 已有明显感知、16 视为「不可能完成」的惩罚档。对普通展示站保持 4 即可,对确认的爬虫群可单独在规则里用 challenge.difficulty 加码;
  • SERVE_ROBOTS_TXT: "true":实测 curl http://127.0.0.1:8133/robots.txt 输出以逐个点名的 User-agent: 段开头的长列表(AddSearchBot、AI2Bot、Amazonbot、AIWebIndex 等),随后 Disallow 全站。上游没能力改 robots.txt 时用它兜底,与策略文件里的 robots-txt ALLOW 规则配合(Anubis 自己应答,不转发上游);
  • COOKIE_SECURE/COOKIE_SAME_SITE 组合:HTTPS 生产站用默认 true/None(跨站 iframe 嵌入场景才需要 COOKIE_PARTITIONED=true 的 CHIPS 分区);本篇 HTTP 演示链路必须 false/Lax,排障实录 6.2 展示了不这么做的后果;
  • SLOG_LEVEL:DEBUG 级别会输出每条请求的规则裁决细节(challenge requested、policy result 等),观测期强烈建议开启;长期运行用 INFO,只保留质询通过与拒绝的 INFO 行。

八、配置文件挂载

本篇 compose 共两处只读挂载,路径约定如下:

宿主路径                          容器路径                        用途
/opt/anubis/botPolicy.yaml   →  /data/cfg/botPolicy.yaml(:ro)  质询策略(POLICY_FNAME 指向)
/opt/anubis/www/             →  /usr/share/nginx/html(:ro)     上游静态站点(演示用)

挂载校验方法:改动 botPolicy.yaml 后重启并确认策略被重新加载:

cd /opt/anubis && docker compose restart anubis
docker compose logs anubis 2>&1 | grep "loading policy file"
{"time":"2026-10-27T17:30:04.728135337Z","level":"INFO","source":{"function":"main.run","file":"github.com/TecharoHQ/anubis/cmd/anubis/main.go","line":308},"msg":"loading policy file","fname":"/data/cfg/botPolicy.yaml"}

fname 显示容器内路径且无解析报错,说明挂载与语法均正确;若 YAML 语法错误,Anubis 启动即失败并在日志给出具体行号。

九、等价启动方式(docker run)

与上述 compose 等价的最小 docker run 形态(上游地址换成可达的 TARGET 即可),便于在无 Compose 的环境手动拉起:

docker run -d \
  --name anubis-1.28.2-server \
  -p 8133:8923 \
  -p 8136:9090 \
  -v /opt/anubis/botPolicy.yaml:/data/cfg/botPolicy.yaml:ro \
  -e BIND=":8923" \
  -e TARGET="http://anubis-1.28.2-backend" \
  -e POLICY_FNAME="/data/cfg/botPolicy.yaml" \
  -e DIFFICULTY="4" \
  -e METRICS_BIND=":9090" \
  -e SERVE_ROBOTS_TXT="true" \
  -e COOKIE_SECURE="false" \
  -e COOKIE_SAME_SITE="Lax" \
  --network anubis-net \
  ghcr.io/techarohq/anubis:v1.28.2

生产推荐 Compose 而非裸 docker run:变量、挂载与健康检查声明式落盘,随 compose.yaml 版本化管理。

十、常用运维命令

cd /opt/anubis

docker compose ps                     # 两服务应为 Up(anubis 应带 healthy)
docker compose logs -f anubis         # 跟踪质询裁决日志(CTRL+C 退出)
docker compose restart anubis         # 改策略文件后重启生效
docker compose down                   # 停止并移除本栈(保留 /opt/anubis 文件)
docker compose up -d                  # 依编排重新拉起
docker compose exec anubis anubis --healthcheck   # 手动触发健康自检

anubis --healthcheck 实测回显(退出码 0):

2026/10/27 17:33:54 running healthcheck

十一、常见问题(含排障实录)

11.1 排障实录一:所有请求返回 500「Oh noes!」,日志报 X-Real-IP header is not set

现象:栈启动 healthy,但 curl 访问 8133 一律 500:

curl -sS -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:8133/
HTTP 500

页面为 Anubis 的 Oh noes! 错误页。查日志定位:

docker compose logs anubis 2>&1 | grep '"level":"ERROR"' | tail -1
{"time":"2026-10-27T17:30:24.225662956Z","level":"ERROR","source":{"function":"github.com/TecharoHQ/anubis/lib.(*Server).maybeReverseProxy","file":"github.com/TecharoHQ/anubis/lib/anubis.go","line":339},"msg":"check failed","subsystem":"anubis","host":"127.0.0.1:8133","method":"GET","path":"/","user_agent":"curl/8.5.0","accept_language":"","priority":"","x-forwarded-for":"","x-real-ip":"","err":"[misconfiguration] X-Real-IP header is not set"}

定位:err 字段写得很直白——[misconfiguration] X-Real-IP header is not set。Anubis 的部署形态是「前置反代 → Anubis」,它默认信任并要求前置层通过 X-Real-IP 告知客户端真实 IP(用于规则裁决、GeoIP 与日志),直连测试时没有任何反代传这个头,就触发此错误。

解决(按部署形态三选一):

  1. 前置 Nginx 加头(官方 nginx 接入样例的标准写法):
location / {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_pass http://anubis;
}
  1. Caddy:
reverse_proxy http://anubis:8923 {
    header_up X-Real-IP {remote_host}
}
  1. 测试/裸跑场景让 Anubis 直接读 TCP 对端地址:编排里加 USE_REMOTE_ADDRESS: "true"。

本篇实测采用模拟反代加头的方式验证:

curl -sS -o /dev/null -w "HTTP %{http_code} (direct pass-through)\n" \
  -H "X-Real-IP: 203.0.113.10" http://127.0.0.1:8133/
HTTP 200 (direct pass-through)

11.2 排障实录二:PoW 计算通过却永远拿不到上游页面(HTTP 站点循环质询)

现象:保持官方默认 COOKIE_SECURE=true(未加 COOKIE_SECURE: "false")跑 HTTP 演示链路,客户端严格按流程解出 PoW、pass-challenge 也正常 302,但重访页面时又被下发新的质询页,无限循环。

复现(模拟真实浏览器:明文 HTTP 上不回传 Secure Cookie):

first visit -> 200 | challenge page: True
PoW solved nonce=165763 in 79ms
pass-challenge -> 302
  set-cookie: techaro.lol-anubis-cookie-verification-eab25f5d secure=True
  set-cookie: techaro.lol-anubis-auth-eab25f5d secure=True
revisit / -> 200 | challenge again: True | upstream ok: False

定位:Anubis 默认签发的 Cookie 带 Secure 标记(要求仅在 HTTPS 传输),而演示链路是明文 HTTP——真实浏览器不会在非安全上下文保存/回传 Secure Cookie,于是每次重访都是「无凭证新会话」,质询白算。这是把「HTTPS 假设」套在 HTTP 部署上的典型错配。

解决:HTTP 环境显式改两项编排变量并重建:

      COOKIE_SECURE: "false"
      COOKIE_SAME_SITE: "Lax"
cd /opt/anubis && docker compose up -d anubis

重跑 4.6 的 PoW 脚本,step4 直接命中上游页面(upstream title: Hello from upstream backend)即修复。HTTPS 生产站务必改回默认值,不要为省事全局关闭 Secure。

11.3 禁用 JavaScript 的客户端会怎样

质询页自带无 JS 降级提示,实测抓取:

<noscript><p>Sadly, you must enable JavaScript to get past this challenge. This is required because AI companies have changed the social contract around how website hosting works. A no-JS solution is a work-in-progress.</p></noscript>

即默认 PoW 质询下禁 JS 客户端无法通过(RSS 阅读器、纯文本浏览器会被拦)。官方提供 metarefresh 无 JS 质询算法(页面定时刷新换凭证,无需执行 JS),代价是对现代无头爬虫的拦截力显著下降;对开放注册的小站点可评估启用,写法见配置详解篇。

11.4 状态码都是 200,怎么监控「被拦了多少」

见 4.7:不要按 HTTP 状态码监控(CHALLENGE/DENY 默认都回 200),改抓 8136 的 anubis_policy_results/anubis_challenges_* 指标,或按日志的 check_result 字段统计。

十二、参考链接