open-space / Dockerfile
kyle-ai's picture
Update Dockerfile
b4ee1ca verified
# 核心镜像:使用 node-slim 保持轻量
FROM node:22-slim
# 1. 整合系统依赖安装
RUN apt-get update && apt-get install -y --no-install-recommends \
git python3 python3-pip ca-certificates procps tzdata \
&& rm -rf /var/lib/apt/lists/*
# 2. 安装 Python 同步依赖 (保持 --break-system-packages 以适配新版镜像)
RUN pip3 install --no-cache-dir huggingface_hub --break-system-packages
# 3. 安装核心程序:合并清理指令
ARG OPENCLAW_VERSION=2026.2.26
RUN npm install -g openclaw@${OPENCLAW_VERSION} --registry=https://registry.npmjs.org/ \
--unsafe-perm=true --foreground-scripts && npm cache clean --force
# 4. 环境变量预设
ENV TZ=Asia/Shanghai \
PORT=7860 \
HOME=/root \
OPENCLAW_TRUST_LOCAL_WS=1 \
OPENCLAW_SECURITY_STRICT=false \
NODE_TLS_REJECT_UNAUTHORIZED=0 \
OPENCLAW_TRUST_PROXY=true \
NODE_ENV=production
# 5. 同步引擎 (修复了 Python 嵌套转义导致的 SyntaxError)
RUN echo 'import os, sys, tarfile, time\n\
from huggingface_hub import HfApi, hf_hub_download\n\
from datetime import datetime, timedelta\n\
api = HfApi()\n\
repo_id = os.getenv("HF_DATASET")\n\
token = os.getenv("HF_TOKEN")\n\
base_dir = "/root"\n\
\n\
def restore():\n\
if not repo_id or not token: return\n\
try:\n\
files = api.list_repo_files(repo_id=repo_id, repo_type="dataset", token=token)\n\
now = datetime.now()\n\
for i in range(5):\n\
day = (now - timedelta(days=i)).strftime("%Y-%m-%d")\n\
name = "backup_" + day + ".tar.gz"\n\
if name in files:\n\
path = hf_hub_download(repo_id=repo_id, filename=name, repo_type="dataset", token=token)\n\
with tarfile.open(path, "r:gz") as tar: tar.extractall(path=base_dir)\n\
print("--- [Sync] ✅ 恢复成功: " + day + " ---")\n\
return True\n\
except Exception as e: print("--- [Sync] ❌ 恢复失败: " + str(e))\n\
\n\
def backup():\n\
if not repo_id or not token: return\n\
try:\n\
target_dir = "/root/.openclaw"\n\
if not os.path.exists(target_dir): return\n\
day_str = datetime.now().strftime("%Y-%m-%d")\n\
name = "backup_" + day_str + ".tar.gz"\n\
with tarfile.open(name, "w:gz") as tar: tar.add(target_dir, arcname=".openclaw")\n\
api.upload_file(path_or_fileobj=name, path_in_repo=name, repo_id=repo_id, repo_type="dataset", token=token)\n\
if os.path.exists(name): os.remove(name)\n\
print("--- [Sync] ✨ 备份完成: " + name + " ---")\n\
except Exception as e: print("--- [Sync] ❌ 备份失败: " + str(e))\n\
\n\
if __name__ == "__main__":\n\
if len(sys.argv) > 1 and sys.argv[1] == "backup": backup()\n\
else: restore()' > /usr/local/bin/sync.py
# 6. 最终启动脚本 (优化配置合并策略:严格深度合并 + 数组按ID智能合并)
RUN echo "#!/bin/bash\n\
mkdir -p /root/.openclaw\n\
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime\n\
\n\
echo \"--- [System] 📦 1. 尝试从云端恢复配置... ---\"\n\
python3 /usr/local/bin/sync.py restore || true\n\
find /root/.openclaw -name \"*.lock\" -delete\n\
chmod 700 /root/.openclaw\n\
\n\
echo \"--- [System] 📝 2. 开始合并默认配置与已恢复的配置... ---\"\n\
export CLEAN_BASE=\$(echo \"\$OPENAI_API_BASE\" | sed \"s|/chat/completions||g\" | sed \"s|/v1/|/v1|g\")\n\
\n\
# 使用 Python 深度合并 JSON (包含数组按ID合并逻辑)\n\
python3 -c \"\n\
import json, os\n\
\n\
def deep_update(d, u):\n\
for k, v in u.items():\n\
if isinstance(v, dict) and k in d and isinstance(d[k], dict):\n\
deep_update(d[k], v)\n\
elif isinstance(v, list) and k in d and isinstance(d[k], list):\n\
# 处理数组合并逻辑\n\
d_dict = {}\n\
non_id_d = []\n\
# 解析原配置(恢复的)数组\n\
for item in d[k]:\n\
if isinstance(item, dict) and 'id' in item:\n\
d_dict[item['id']] = item\n\
else:\n\
non_id_d.append(item)\n\
\n\
non_id_u = []\n\
# 解析新配置(环境变量)数组并进行合并\n\
for item in v:\n\
if isinstance(item, dict) and 'id' in item:\n\
if item['id'] in d_dict:\n\
# id相同,对内部字段继续深度合并(以新配置为准覆盖)\n\
d_dict[item['id']] = deep_update(d_dict[item['id']], item)\n\
else:\n\
d_dict[item['id']] = item\n\
else:\n\
# 对于普通元素(如字符串IP),去重合并\n\
if item not in non_id_d:\n\
non_id_u.append(item)\n\
\n\
# 组合合并后的数组\n\
d[k] = list(d_dict.values()) + non_id_d + non_id_u\n\
else:\n\
d[k] = v\n\
return d\n\
\n\
config_path = '/root/.openclaw/openclaw.json'\n\
current_config = {}\n\
\n\
if os.path.exists(config_path):\n\
try:\n\
with open(config_path, 'r', encoding='utf-8') as f:\n\
current_config = json.load(f)\n\
except Exception:\n\
pass\n\
\n\
clean_base = os.environ.get('CLEAN_BASE', '')\n\
api_key = os.environ.get('OPENAI_API_KEY', '')\n\
model = os.environ.get('MODEL', '')\n\
gateway_pw = os.environ.get('OPENCLAW_GATEWAY_PASSWORD', '')\n\
\n\
default_config = {\n\
'models': {\n\
'providers': {\n\
'siliconflow': {\n\
'baseUrl': clean_base,\n\
'apiKey': api_key,\n\
'api': 'openai-completions',\n\
'authHeader': True,\n\
'models': [{'id': model, 'name': 'DeepSeek', 'contextWindow': 128000}]\n\
}\n\
}\n\
},\n\
'agents': {\n\
'defaults': {\n\
'model': {'primary': 'siliconflow/' + model}\n\
}\n\
},\n\
'gateway': {\n\
'mode': 'local', 'port': 7860, 'bind': 'custom', 'customBindHost': '0.0.0.0',\n\
'trustedProxies': ['10.0.0.0/8'],\n\
'auth': {'mode': 'token', 'token': gateway_pw},\n\
'controlUi': {\n\
'enabled': True,\n\
'allowInsecureAuth': True,\n\
'dangerouslyDisableDeviceAuth': True,\n\
'dangerouslyAllowHostHeaderOriginFallback': True\n\
},\n\
'tools': {'deny': ['gateway']}\n\
}\n\
}\n\
\n\
merged_config = deep_update(current_config, default_config)\n\
\n\
with open(config_path, 'w', encoding='utf-8') as f:\n\
json.dump(merged_config, f, indent=2, ensure_ascii=False)\n\
\"\n\
echo \"--- [System] ✅ 配置合并完成。 ---\"\n\
\n\
# 启动后台备份任务\n\
(while true; do sleep 1800; python3 /usr/local/bin/sync.py backup; done) &\n\
\n\
export NODE_ENV=production\n\
export OPENCLAW_TRUST_PROXY=true\n\
\n\
echo \"--- [System] 🚀 3. 正在启动 OpenClaw Gateway... ---\"\n\
set +e\n\
trap 'kill -TERM \$PID 2>/dev/null' TERM INT\n\
\n\
while true; do\n\
find /root/.openclaw -name \"*.lock\" -delete\n\
\n\
openclaw gateway run --port 7860 &\n\
PID=\$!\n\
wait \$PID\n\
\n\
echo \"--- [System] ⚠️ OpenClaw 主进程退出,正在清理环境准备重启... ---\"\n\
openclaw gateway stop || true\n\
pkill -f openclaw || true\n\
pkill -f node || true\n\
\n\
python3 /usr/local/bin/sync.py backup || true\n\
sleep 3\n\
done\n\
" > /usr/local/bin/start-openclaw && chmod +x /usr/local/bin/start-openclaw
EXPOSE 7860
CMD ["/usr/local/bin/start-openclaw"]