openclawbot / Dockerfile
zhengr's picture
Update Dockerfile
c5664f4 verified
# ============================================================
# OpenClaw HF Space Dockerfile
# 修复版:解决 hf.space 拒绝连接问题
# Build: 2026-02-13-v44 (3h Backup Schedule)
# ============================================================
# 核心镜像选择
FROM node:22-slim
# 1. 基础依赖补全
RUN apt-get update && apt-get install -y --no-install-recommends \
git openssh-client build-essential python3 python3-pip \
g++ make ca-certificates nginx \
&& rm -rf /var/lib/apt/lists/*
# 2. 安装 HF 数据交互工具
RUN pip3 install --no-cache-dir huggingface_hub --break-system-packages
# 3. 构建环境与 Git 协议优化
RUN update-ca-certificates && \
git config --global http.sslVerify false && \
git config --global url."https://github.com/".insteadOf ssh://git@github.com/
# 4. OpenClaw 核心安装
RUN npm install -g openclaw@latest --unsafe-perm
# 5. 环境变量预设
ENV PORT=7860 \
HOME=/root \
OPENCLAW_INTERNAL_PORT=18789
# 6. Python 同步引擎 (sync.py)
RUN cat > /usr/local/bin/sync.py << 'EOF'
import os, sys, tarfile
from huggingface_hub import HfApi, hf_hub_download
from datetime import datetime
api = HfApi()
repo_id = os.getenv("HF_DATASET")
token = os.getenv("HF_TOKEN")
def restore():
try:
print(f"[SYNC] restore start repo={repo_id}")
if not repo_id or not token:
print("[SYNC] restore skipped: HF_DATASET or HF_TOKEN missing")
return False
files = api.list_repo_files(repo_id=repo_id, repo_type="dataset", token=token)
candidates = [f for f in files if f.startswith("backup_") and f.endswith(".tar.gz")]
for name in sorted(candidates, reverse=True):
print(f"[SYNC] restore downloading {name}")
path = hf_hub_download(repo_id=repo_id, filename=name, repo_type="dataset", token=token)
with tarfile.open(path, "r:gz") as tar:
tar.extractall(path="/root/.openclaw/")
print(f"Success: Restored from {name}")
return True
print("[SYNC] restore no backups found")
except Exception as e:
print(f"Restore Error: {e}")
def backup():
try:
if not repo_id or not token:
print("[SYNC] backup skipped: HF_DATASET or HF_TOKEN missing")
return False
stamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
name = f"backup_{stamp}.tar.gz"
print(f"[SYNC] backup start {name}")
with tarfile.open(name, "w:gz") as tar:
if os.path.exists("/root/.openclaw/sessions"):
tar.add("/root/.openclaw/sessions", arcname="sessions")
if os.path.exists("/root/.openclaw/openclaw.json"):
tar.add("/root/.openclaw/openclaw.json", arcname="openclaw.json")
api.upload_file(path_or_fileobj=name, path_in_repo=name, repo_id=repo_id, repo_type="dataset", token=token)
print(f"Backup {name} Success.")
return True
except Exception as e:
print(f"Backup Error: {e}")
return False
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "backup":
backup()
else:
restore()
EOF
RUN chmod +x /usr/local/bin/sync.py
# 7. 启动控制逻辑
RUN cat > /usr/local/bin/start-openclaw << 'EOF'
#!/bin/bash
set -e
mkdir -p /root/.openclaw/sessions
mkdir -p /root/.openclaw/agents/main/sessions
mkdir -p /root/.openclaw/credentials
chmod 700 /root/.openclaw
# 阶段 1: 执行启动前恢复
# python3 /usr/local/bin/sync.py restore
# 阶段 2: 生成网关与模型配置
# 使用 Nginx 反代模式:OpenClaw 监听 loopback (18789),Nginx 监听外部 (7860)
cat > /root/.openclaw/openclaw.json << 'JSONEOF'
{
"models": {
"providers": {
"nvidia": {
"baseUrl": "https://integrate.api.nvidia.com/v1",
"apiKey": "__NVIDIA_API_KEY__",
"api": "openai-completions",
"models": [
{
"id": "moonshotai/kimi-k2.5",
"name": "Kimi-K2.5",
"contextWindow": 128000
}
]
},
"siliconflow": {
"baseUrl": "https://api.siliconflow.cn/v1",
"apiKey": "__SILICONFLOW_API_KEY__",
"api": "openai-completions",
"models": [
{
"id": "deepseek-ai/DeepSeek-V3",
"name": "DeepSeek-V3",
"contextWindow": 128000
}
]
}
}
},
"agents": {
"defaults": {
"model": {
"primary": "nvidia/moonshotai/kimi-k2.5",
"fallbacks": ["siliconflow/deepseek-ai/DeepSeek-V3"]
}
}
},
"gateway": {
"mode": "local",
"bind": "lan",
"port": __PORT__,
"trustedProxies": ["10.0.0.0/8", "127.0.0.1", "::1"],
"auth": {
"mode": "token",
"token": "__OPENCLAW_GATEWAY_TOKEN__"
},
"controlUi": {
"allowInsecureAuth": true,
"allowedOrigins": ["__CONTROL_UI_ORIGIN__"],
"dangerouslyDisableDeviceAuth": true
}
}
}
JSONEOF
# 环境变量处理 (强制 token 模式)
export OPENCLAW_GATEWAY_AUTH_MODE=token
# 优先使用 OPENCLAW_GATEWAY_TOKEN,如果未设置则使用默认值 123456
export OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN:-"123456"}
# 清除密码变量防止混淆
unset OPENCLAW_GATEWAY_PASSWORD
# 修复 baseUrl 可能包含的反引号和空格
sed -i 's|`||g' /root/.openclaw/openclaw.json
sed -i 's|[[:space:]]"https|"https|g' /root/.openclaw/openclaw.json
sed -i "s|__NVIDIA_API_KEY__|$NVIDIA_API_KEY|g" /root/.openclaw/openclaw.json
sed -i "s|__SILICONFLOW_API_KEY__|$SILICONFLOW_API_KEY|g" /root/.openclaw/openclaw.json
# OpenClaw 监听内部端口 18789
sed -i "s|__PORT__|$OPENCLAW_INTERNAL_PORT|g" /root/.openclaw/openclaw.json
sed -i "s|__OPENCLAW_GATEWAY_TOKEN__|$OPENCLAW_GATEWAY_TOKEN|g" /root/.openclaw/openclaw.json
sed -i "s|__OPENCLAW_GATEWAY_AUTH_MODE__|$OPENCLAW_GATEWAY_AUTH_MODE|g" /root/.openclaw/openclaw.json
CONTROL_UI_ORIGIN="${SPACE_HOST:-${SPACE_DOMAIN:-${HF_SPACE_HOST:-}}}"
if [ -n "$CONTROL_UI_ORIGIN" ]; then
CONTROL_UI_ORIGIN="https://$CONTROL_UI_ORIGIN"
else
CONTROL_UI_ORIGIN="http://127.0.0.1:$OPENCLAW_INTERNAL_PORT"
fi
sed -i "s|__CONTROL_UI_ORIGIN__|$CONTROL_UI_ORIGIN|g" /root/.openclaw/openclaw.json
# 设置权限
chmod 600 /root/.openclaw/openclaw.json
# 打印配置
echo "========================================"
cat /root/.openclaw/openclaw.json
echo "========================================"
echo "✅ OpenClaw Gateway is starting on internal port $OPENCLAW_INTERNAL_PORT..."
echo "✅ Nginx is starting on external port ${PORT:-7860}..."
echo "Auth Mode: $OPENCLAW_GATEWAY_AUTH_MODE"
echo "🔑 Token: $OPENCLAW_GATEWAY_TOKEN"
# 启动 OpenClaw (监听 lan,允许 Nginx 转发,通过防火墙/Docker 隔离外部访问)
OPENCLAW_GATEWAY_RUN_ARGS="--port $OPENCLAW_INTERNAL_PORT --bind lan --auth token --token $OPENCLAW_GATEWAY_TOKEN"
# 每 3 小时自动备份一次
( while true; do sleep 10800; python3 /usr/local/bin/sync.py backup; done ) &
# 后台运行 OpenClaw
openclaw gateway run $OPENCLAW_GATEWAY_RUN_ARGS --allow-unconfigured &
python3 - << 'PY'
import socket, time, sys
host = "127.0.0.1"
port = 18789
deadline = time.time() + 30
while time.time() < deadline:
try:
with socket.create_connection((host, port), timeout=2):
sys.exit(0)
except Exception:
time.sleep(1)
sys.exit(1)
PY
# 生成 Nginx 配置
cat > /etc/nginx/conf.d/openclaw.conf << NGINXEOF
map \$http_upgrade \$connection_upgrade {
default upgrade;
'' close;
}
server {
listen ${PORT:-7860};
listen [::]:${PORT:-7860};
# 健康检查端点 (验证 Nginx 状态)
location /health {
access_log off;
return 200;
}
# 强制大超时,防止长连接断开
proxy_read_timeout 86400;
proxy_send_timeout 86400;
location / {
# 移除自动重定向,让用户能看到登录界面
# if ($request_uri = "/") {
# return 302 /?token=${OPENCLAW_GATEWAY_TOKEN:-123456};
# }
proxy_pass http://127.0.0.1:${OPENCLAW_INTERNAL_PORT};
proxy_http_version 1.1;
# 动态 WebSocket 配置 (关键修复:防止静态资源被错误升级)
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection \$connection_upgrade;
# 恢复 Host 透传,因为伪造 Host 可能导致 OpenClaw 认为我们在访问错误的虚拟主机
proxy_set_header Host \$host;
# 恢复 Origin 透传,依赖 allowedOrigins: ["__CONTROL_UI_ORIGIN__"]
proxy_set_header Origin \$http_origin;
# 透传真实来源 IP
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Port ${PORT:-7860};
# ⚠️ 强制声明 HTTPS 协议
proxy_set_header X-Forwarded-Proto https;
# 禁用缓冲 (对 WebSocket 至关重要)
proxy_buffering off;
proxy_cache off;
# 解除 iframe 限制
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "" always;
proxy_hide_header Content-Security-Policy;
add_header Content-Security-Policy "frame-ancestors *" always;
}
}
NGINXEOF
echo "Nginx Config:"
cat /etc/nginx/conf.d/openclaw.conf
nginx -t
# 前台运行 Nginx (不要使用 exec,因为这会替换 shell 进程)
nginx -g "daemon off;" &
# 保持主进程活跃并输出日志
echo "🚀 Services started. Tailing logs..."
tail -f /var/log/nginx/access.log /var/log/nginx/error.log &
wait
EOF
RUN chmod +x /usr/local/bin/start-openclaw
# 8. ⚠️ 关键:暴露端口(HF Space 必须)
EXPOSE 7860
# 启动命令 (Build: 2026-02-13-v35 Fix Startup Silent Exit)
# 增加日志输出,确保前台进程不退出
CMD ["/bin/bash", "-c", "/usr/local/bin/start-openclaw && tail -f /dev/null"]