前言

最近在捣鼓Astrbot,部署了Astrbot官方的shipyard-neo实现了让bot使用电脑。
不过在实际使用过程中,我发现官方的默认配置有两大问题

  • 默认浏览器的ua明文headless
  • 如果是服务器,ip也可能是idc的

以上两个问题导致bot在使用浏览器时有可能被风控,导致浏览器形同虚设,在实际使用时我发现bot使用浏览器访问Google search被风控。

通过修改官方的gull镜像(截至2026年6月20日),我成功地移除了headless chrome ua中的headless字样,同时通过部分配置给gull中的无头浏览器套上了干净的代理,通过这两个手段降低了ai agent无头浏览器被风控的概率。

以下是我实现用的编排和配置,有需要可以参考一下,希望可以帮到大家!
注意,其中shipyard-neo gull被我替换成了我的自定义镜像,以移除默认ua中的headless,如果官方后续有变动可以换成官方的service参考。

配置

docker compose

services:
    # ── Gull Service (shared browser pool) ─────────────────
    # Shared Chromium for all browser-python sandboxes.  One Chromium
    # process serves all sandboxes instead of per-sandbox Gull containers.
    mihomo:
        container_name: mihomo
        image: metacubex/mihomo # 官方核心镜像
        cap_add:
            - NET_ADMIN # gvisor 需要修改容器内路由表
        restart: unless-stopped
        ports:
            - "代理端口:代理端口" # HTTP/SOCKS5 混合代理端口
            - "webui:webui"
        volumes:
            - /opt/work/mihomo/config:/root/.config/mihomo # 映射配置目录
        devices:
            - /dev/net/tun:/dev/net/tun
        dns:
            - 198.18.0.1
        # dns_search: .
        networks:
            - bay-network

    astrbot:
        image: soulter/astrbot:latest
        container_name: astrbot
        restart: unless-stopped
        ports:
            - "${ASTRBOT_PORT:-6185}:6185"
        volumes:
            # AstrBot persistent data
            - /opt/work/astrbot/data:/AstrBot/data
            # Bay credentials auto-discovery (read-only)
            - bay-data:/bay-data:ro
        environment:
            # Tell _discover_bay_credentials() where to find credentials.json
            - BAY_DATA_DIR=/bay-data
        depends_on:
            bay:
                condition: service_healthy
        networks:
            - bay-network
        logging:
            driver: json-file
            options:
                max-size: "50m"
                max-file: "3"

    gull-service:
        # image: ghcr.io/astrbotdevs/shipyard-neo-gull:latest
        image: nicocatxzc/shipyard-neo-gull:latest
        container_name: bay-gull
        restart: unless-stopped
        environment:
            - GULL_MODE=shared
            - GULL_CDP_PORT=9222
            - AGENT_BROWSER_IDLE_TIMEOUT_MS=600000 # 10 min session GC
            - GULL_CHROMIUM_ARGS=--user-agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36" #custom user-agent
        volumes:
            - ./bay-cargos:/cargos:ro
        # networks:
        #   - bay-network
        network_mode: "container:mihomo"
        depends_on:
            - mihomo

    bay:
        image: ghcr.io/astrbotdevs/shipyard-neo-bay:latest
        container_name: bay
        restart: unless-stopped
        ports:
            - "8114:8114"
        volumes:
            # Docker socket — Bay creates sandbox containers dynamically
            - /var/run/docker.sock:/var/run/docker.sock
            # Config file
            - /opt/work/astrbot/config.yaml:/app/config.yaml:ro
            # SQLite database persistence
            - bay-data:/app/data
            # Cargo storage persistence (bind mount — shared with gull-service)
            - ./bay-cargos:/var/lib/bay/cargos
        environment:
            - BAY_CONFIG_FILE=/app/config.yaml
            - BAY_DATA_DIR=/app/data
            # To inject a fixed API key instead of auto-generation:
            # - BAY_API_KEY=sk-bay-your-key-here
        networks:
            - bay-network
        healthcheck:
            test: ["CMD", "curl", "-f", "http://localhost:8114/health"]
            interval: 30s
            timeout: 10s
            retries: 5
            start_period: 15s
        logging:
            driver: json-file
            options:
                max-size: "50m"
                max-file: "5"

networks:
    bay-network:
        name: bay-network
        driver: bridge

volumes:
    bay-data:
        name: bay-data

bay配置

server:
  host: "0.0.0.0"
  port: 8114

database:
  # SQLite for single-instance deployment.
  # For HA / multi-instance, switch to PostgreSQL:
  #   url: "postgresql+asyncpg://user:pass@db-host:5432/bay"
  url: "sqlite+aiosqlite:///./data/bay.db"
  echo: false

driver:
  type: docker

  # Pull latest images when creating new sandboxes.
  # Production recommendation: "always" ensures you always get the latest image.
  image_pull_policy: always

  docker:
    socket: "unix:///var/run/docker.sock"

    # Bay in container, Ship/Gull in container — use container network direct connect
    connect_mode: container_network

    # Shared network name (must match docker-compose.yaml network)
    network: "bay-network"

    # Disable host port mapping — sandbox containers don't need to be reachable
    # from outside the Docker network, reducing attack surface.
    publish_ports: false
    host_port: null

cargo:
  root_path: "/var/lib/bay/cargos"
  default_size_limit_mb: 1024
  mount_path: "/workspace"

security:
  # CHANGE-ME: 设置一个强随机密钥 (e.g. `openssl rand -hex 32`)
  api_key: "openssl"
  allow_anonymous: false

# ── Shared Browser Service ───────────────────────────────
# One Chromium process serves all browser-python sandboxes via gull-service.
# Set enabled: false to revert to per-sandbox Gull containers.
browser_service:
  enabled: true
  endpoint: "http://mihomo:8115"

# Container proxy environment injection.
# When enabled, Bay injects HTTP(S)_PROXY and NO_PROXY into sandbox containers.
proxy:
  enabled: false
  # http_proxy: "http://proxy.example.com:7890"
  # https_proxy: "http://proxy.example.com:7890"
  # Optional extra entries to append to default NO_PROXY list
  # no_proxy: "my-internal.service"

# Warm Pool — pre-start standby sandbox instances to reduce cold-start latency.
# When a user creates a sandbox, Bay will first try to claim an available warm instance,
# delivering near-instant startup instead of waiting for container boot.
warm_pool:
  enabled: true
  warmup_queue_workers: 2          # Concurrent warmup workers
  warmup_queue_max_size: 256       # Maximum queue depth
  warmup_queue_drop_policy: "drop_newest"
  warmup_queue_drop_alert_threshold: 50
  interval_seconds: 30             # Pool maintenance scan interval
  run_on_startup: true

profiles:
  # ── Standard Python sandbox ────────────────────────
  - id: python-default
    description: "Standard Python sandbox with filesystem and shell access"
    image: "ghcr.io/astrbotdevs/shipyard-neo-ship:latest"
    runtime_type: ship
    runtime_port: 8123
    resources:
      cpus: 1.0
      memory: "1g"
    capabilities:
      - filesystem  # includes upload/download
      - shell
      - python
    idle_timeout: 1800  # 30 minutes
    warm_pool_size: 0   # Keep 1 pre-warmed instance ready
    # Environment variables injected into the runtime container (available in Python and Shell)
    # Example: env: { TZ: "Asia/Shanghai", LANG: "en_US.UTF-8", CUSTOM_VAR: "value" }
    env: {}
    # Optional profile-level proxy override
    # proxy:
    #   enabled: false

  # ── Data Science sandbox (more resources) ──────────
  - id: python-data
    description: "Data science sandbox with extra CPU and memory"
    image: "ghcr.io/astrbotdevs/shipyard-neo-ship:latest"
    runtime_type: ship
    runtime_port: 8123
    resources:
      cpus: 2.0
      memory: "4g"
    capabilities:
      - filesystem  # includes upload/download
      - shell
      - python
    idle_timeout: 1800
    warm_pool_size: 0
    env: {}

  # ── Browser + Python (shared browser pool) ──────────
  - id: browser-python
    description: "Browser automation with Python backend (shared Chromium)"
    browser: shared  # routes through gull-service, not per-sandbox gull
    containers:
      - name: ship
        image: "ghcr.io/astrbotdevs/shipyard-neo-ship:latest"
        runtime_type: ship
        runtime_port: 8123
        resources:
          cpus: 2.0
          memory: "2g"
        capabilities:
          - python
          - shell
          - filesystem
          - browser
        env: {}
    idle_timeout: 1800
    warm_pool_size: 1

gc:
  # Enable automatic GC for production
  enabled: true
  run_on_startup: true
  interval_seconds: 180  # 3 minutes

  # Instance identifier — MUST be unique in multi-instance deployments
  instance_id: "bay-prod"

  idle_session:
    enabled: true
  expired_sandbox:
    enabled: true
  orphan_cargo:
    enabled: true
  orphan_container:
    # Enable in production to clean up leaked containers.
    # Safe as long as instance_id is unique per Bay instance.
    enabled: true

mihomo配置

mode: rule
mixed-port: 7897
allow-lan: true # 允许内网调用
log-level: info
ipv6: false # ipv6
unified-delay: true

# 开启外部控制API
external-controller: 0.0.0.0:port # 访问端口
secret: "passwd" # 访问密码
# 允许面板跨域
external-controller-cors:
    allow-origins:
        - "*"
    allow-private-network: true

profile:
    store-selected: true

# tun配置
tun:
    enable: true
    stack: gvisor
    auto-route: true
    strict-route: false
    auto-detect-interface: true
    dns-hijack:
        - any:53

# dns
dns:
    enable: true
    listen: :53
    enhanced-mode: fake-ip
    fake-ip-range: 198.18.0.1/16
    fake-ip-filter:
        - "*.lan"
        - "*.local"
        - "*.arpa"
        - "+.msftncsi.com"
        - "www.msftconnecttest.com"

    # 默认dns
    default-nameserver:
        - 1.1.1.1
        - 8.8.8.8

    # 加密dns
    nameserver:
        - tls://1.1.1.1
        - https://dns.google/dns-query
        - tls://dns.adguard.com

    # 防污染的 Fallback 走 DoH
    fallback:
        - https://cloudflare-dns.com/dns-query
        - tls://8.8.8.8

    fallback-filter:
        geoip: true
        geoip-code: CN
        ipcidr:
            - 240.0.0.0/4
            - 0.0.0.0/32

# 节点列表
proxies:
    - name: "节点"

proxy-groups:
    - name: PROXY
      type: select
      proxies:
          - "节点"

# 规则
rules:
    #ipapi
    - DOMAIN-KEYWORD,ip-api,PROXY
    # === 1.核心风控 ===
    - DOMAIN,accounts.google.com,PROXY
    - DOMAIN,chat.openai.com,PROXY
    - DOMAIN,auth0.openai.com,PROXY
    - DOMAIN,challenges.cloudflare.com,PROXY
    - DOMAIN-SUFFIX,google.com,PROXY
    - DOMAIN-SUFFIX,gmail.com,PROXY
    - DOMAIN-SUFFIX,openai.com,PROXY
    - DOMAIN-KEYWORD,recaptcha,PROXY
    - DOMAIN-KEYWORD,turnstile,PROXY

    # === 社媒 ===
    - DOMAIN-SUFFIX,facebook.com,PROXY
    - DOMAIN-SUFFIX,instagram.com,PROXY
    - DOMAIN-SUFFIX,twitter.com,PROXY

    # === 内网 ===
    - IP-CIDR,127.0.0.0/8,DIRECT
    - IP-CIDR,10.0.0.0/8,DIRECT
    - IP-CIDR,172.16.0.0/12,DIRECT
    - IP-CIDR,192.168.0.0/16,DIRECT
    - GEOIP,CN,DIRECT

    - MATCH,DIRECT