| FROM node:22-slim |
|
|
| # Install system dependencies |
| RUN apt-get update && apt-get install -y --no-install-recommends \ |
| git openssh-client build-essential python3 python3-pip \ |
| g++ make ca-certificates curl chromium tzdata \ |
| libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \ |
| libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 \ |
| libgbm1 libasound2 libpangocairo-1.0-0 libpango-1.0-0 \ |
| && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ |
| && echo "Asia/Shanghai" > /etc/timezone \ |
| && rm -rf /var/lib/apt/lists/* |
|
|
| # Install Python packages |
| RUN pip3 install --no-cache-dir huggingface_hub --break-system-packages |
|
|
| # Git and CA config |
| RUN update-ca-certificates && \ |
| git config --global http.sslVerify false && \ |
| git config --global url."https://github.com/".insteadOf ssh://git@github.com/ |
|
|
| ENV HOME=/root |
| ENV PORT=7860 \ |
| OPENCLAW_GATEWAY_MODE=local \ |
| OPENCLAW_BROWSER_PATH=/usr/bin/chromium |
|
|
| # Install OpenClaw |
| RUN npm install -g openclaw@latest zod --unsafe-perm |
|
|
| # Generate sync.py using a Heredoc to avoid syntax/escaping issues |
| RUN cat <<'EOF' > /usr/local/bin/sync.py |
| import os, sys, tarfile |
| from huggingface_hub import HfApi, hf_hub_download |
| from datetime import datetime, timedelta |
|
|
| api = HfApi() |
| repo_id = os.getenv("HF_DATASET") |
| token = os.getenv("HF_TOKEN") |
|
|
| def restore(): |
| try: |
| print(f"--- [SYNC] Start recovery process, target repository: {repo_id} ---") |
| if repo_id and token: |
| files = api.list_repo_files(repo_id=repo_id, repo_type="dataset", token=token) |
| now = datetime.now() |
| found = False |
| for i in range(5): |
| day = (now - timedelta(days=i)).strftime("%Y-%m-%d") |
| name = f"backup_{day}.tar.gz" |
| if name in files: |
| print(f"--- [SYNC] Backup file found: {name}, Downloading... ---") |
| 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"--- [SYNC] Recovery successful! ---") |
| found = True |
| break |
| if not found: |
| print("--- [SYNC] No backup packages found for the last 5 days---") |
| else: |
| print("--- [SYNC] Skip recovery: HF_DATASET or HF_TOKEN not configured ---") |
|
|
| # Clean up .lock files |
| count = 0 |
| for root, dirs, fs in os.walk("/root/.openclaw/"): |
| for f in fs: |
| if f.endswith(".lock"): |
| try: |
| os.remove(os.path.join(root, f)) |
| count += 1 |
| except: pass |
| if count > 0: |
| print(f"--- [SYNC] {count} residual lock files cleaned up---") |
| return True |
| except Exception as e: |
| print(f"--- [SYNC] Recovery Exception: {e} ---") |
|
|
| def backup(): |
| try: |
| day = datetime.now().strftime("%Y-%m-%d") |
| name = f"backup_{day}.tar.gz" |
| print(f"--- [SYNC] Performing a full backup: {name} ---") |
| def lock_filter(tarinfo): |
| if tarinfo.name.endswith(".lock"): return None |
| return tarinfo |
| with tarfile.open(name, "w:gz") as tar: |
| for target in ["sessions", "workspace", "agents", "memory", "plugins", "openclaw.json"]: |
| full_path = f"/root/.openclaw/{target}" |
| if os.path.exists(full_path): |
| tar.add(full_path, arcname=target, filter=lock_filter) |
| if repo_id and token: |
| api.upload_file(path_or_fileobj=name, path_in_repo=name, repo_id=repo_id, repo_type="dataset", token=token) |
| print("--- [SYNC] Backup upload successful! ---") |
| except Exception as e: |
| print(f"--- [SYNC] Backup failed: {e} ---") |
|
|
| if __name__ == "__main__": |
| if len(sys.argv) > 1 and sys.argv[1] == "backup": |
| backup() |
| else: |
| restore() |
| EOF |
|
|
| # Create startup script |
| RUN cat <<'EOF' > /usr/local/bin/start-openclaw |
| #!/bin/bash |
| set -e |
| mkdir -p /root/.openclaw/{sessions,workspace,plugins,agents/main/sessions,credentials} |
| ln -s /root/.openclaw/workspace /root/.openclaw/memory || true |
| chmod 700 /root/.openclaw || true |
|
|
| python3 /usr/local/bin/sync.py restore || true |
|
|
| export OPENCLAW_GATEWAY_TOKEN="${OPENCLAW_GATEWAY_PASSWORD}" |
| CLEAN_BASE=$(echo "${OPENAI_API_BASE}" | sed "s|/chat/completions||g" | sed "s|/v1/|/v1|g" | sed "s|/v1$|/v1|g") |
|
|
| cat > /root/.openclaw/openclaw.json <<EOC |
| { |
| "models": { |
| "mode": "merge", |
| "providers": { |
| "zai": { |
| "baseUrl": "${CLEAN_BASE}", |
| "apiKey": "${OPENAI_API_KEY}", |
| "api": "openai-completions", |
| "models": [ |
| {"id": "glm-4.7", "name": "GLM-4.7 Coding", "contextWindow": 200000, "maxTokens": 8192}, |
| {"id": "glm-5", "name": "GLM-5", "contextWindow": 200000, "maxTokens": 8192} |
| ] |
| } |
| } |
| }, |
| "agents": { |
| "defaults": { |
| "model": {"primary": "zai/glm-4.7"}, |
| "workspace": "~/.openclaw/workspace" |
| } |
| }, |
| "gateway": { |
| "mode": "local", "bind": "lan", "port": ${PORT}, |
| "trustedProxies": ["0.0.0.0/0"], |
| "auth": { "mode": "token", "token": "${OPENCLAW_GATEWAY_PASSWORD}" }, |
| "controlUi": { "allowInsecureAuth": true } |
| } |
| } |
| EOC |
|
|
| (while true; do sleep 10800; python3 /usr/local/bin/sync.py backup || true; done) & |
|
|
| openclaw doctor --fix || true |
| exec openclaw gateway run --port ${PORT} |
| EOF |
|
|
| RUN chmod +x /usr/local/bin/start-openclaw |
|
|
| EXPOSE 7860 |
| CMD ["/usr/local/bin/start-openclaw"] |