Docs: add Docker backup and cross-arch migration scripts
This commit is contained in:
parent
49c7fba483
commit
a9ce49c5f3
@ -624,6 +624,89 @@ docker compose run --rm openclaw-cli devices list --url ws://127.0.0.1:18789
|
||||
- Dockerfile CMD uses `--allow-unconfigured`; mounted config with `gateway.mode` not `local` will still start. Override CMD to enforce the guard.
|
||||
- The gateway container is the source of truth for sessions (`~/.openclaw/agents/<agentId>/sessions/`).
|
||||
|
||||
## Backup and migration (Intel Mac to Apple Silicon)
|
||||
|
||||
For low-disruption host migration, move OpenClaw data/config and rebuild Docker
|
||||
images natively on the new machine.
|
||||
|
||||
Use:
|
||||
|
||||
- `scripts/migrate/backup-openclaw.sh` on source host
|
||||
- `scripts/migrate/restore-openclaw.sh` on target host
|
||||
|
||||
### 1) Create backup on source host
|
||||
|
||||
From repo root:
|
||||
|
||||
```bash
|
||||
scripts/migrate/backup-openclaw.sh
|
||||
```
|
||||
|
||||
The archive includes:
|
||||
|
||||
- OpenClaw config dir (`OPENCLAW_CONFIG_DIR` or `~/.openclaw`)
|
||||
- OpenClaw workspace dir (`OPENCLAW_WORKSPACE_DIR` or `~/.openclaw/workspace`)
|
||||
- `.env` and Docker setup files from repo root
|
||||
- metadata + internal checksum manifest
|
||||
|
||||
Output files:
|
||||
|
||||
- `backups/openclaw-backup-<timestamp>.tar.gz`
|
||||
- `backups/openclaw-backup-<timestamp>.tar.gz.sha256`
|
||||
|
||||
Optional path overrides:
|
||||
|
||||
```bash
|
||||
scripts/migrate/backup-openclaw.sh \
|
||||
--config-dir "$HOME/.openclaw" \
|
||||
--workspace-dir "$HOME/.openclaw/workspace" \
|
||||
--output-dir "$HOME/openclaw-backups"
|
||||
```
|
||||
|
||||
### 2) Transfer archive to target host
|
||||
|
||||
Copy archive + checksum file to the new Mac using your normal secure transfer
|
||||
method (scp, encrypted disk, etc.).
|
||||
|
||||
### 3) Restore on target host
|
||||
|
||||
From repo root on target host:
|
||||
|
||||
```bash
|
||||
scripts/migrate/restore-openclaw.sh --archive /path/to/openclaw-backup-<timestamp>.tar.gz
|
||||
```
|
||||
|
||||
Default restore behavior:
|
||||
|
||||
- verifies archive checksums
|
||||
- stops `openclaw-gateway` container first
|
||||
- snapshots current config/workspace as `.pre-restore-<timestamp>`
|
||||
- restores config/workspace from backup
|
||||
- writes backup env file as `.env.from-backup` for review
|
||||
|
||||
To overwrite `.env` directly:
|
||||
|
||||
```bash
|
||||
scripts/migrate/restore-openclaw.sh \
|
||||
--archive /path/to/openclaw-backup-<timestamp>.tar.gz \
|
||||
--apply-env
|
||||
```
|
||||
|
||||
### 4) Rebuild and validate on target arch
|
||||
|
||||
Always rebuild on Apple Silicon:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build --force-recreate openclaw-gateway
|
||||
docker compose run --rm openclaw-cli health
|
||||
docker compose run --rm openclaw-cli channels status --probe
|
||||
```
|
||||
|
||||
### Architecture migration note
|
||||
|
||||
Do not carry over architecture-specific binary caches from x86 to arm hosts.
|
||||
Rebuild containers and reinstall native toolchains on the target host.
|
||||
|
||||
## Agent Sandbox (host gateway + Docker tools)
|
||||
|
||||
Deep dive: [Sandboxing](/gateway/sandboxing)
|
||||
|
||||
208
scripts/migrate/backup-openclaw.sh
Executable file
208
scripts/migrate/backup-openclaw.sh
Executable file
@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/migrate/backup-openclaw.sh [options]
|
||||
|
||||
Options:
|
||||
--repo-root <path> OpenClaw repo root (default: current repo)
|
||||
--env-file <path> Env file to include (default: <repo-root>/.env)
|
||||
--config-dir <path> OpenClaw config dir (default: env or ~/.openclaw)
|
||||
--workspace-dir <path> OpenClaw workspace dir (default: env or ~/.openclaw/workspace)
|
||||
--output-dir <path> Output directory for backup archive (default: <repo-root>/backups)
|
||||
--name <name> Backup name prefix (default: openclaw-backup-<timestamp>)
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "Missing required command: $1"
|
||||
}
|
||||
|
||||
strip_quotes() {
|
||||
local value="$1"
|
||||
if [[ "${value}" == \"*\" && "${value}" == *\" ]]; then
|
||||
value="${value:1:${#value}-2}"
|
||||
elif [[ "${value}" == \'*\' && "${value}" == *\' ]]; then
|
||||
value="${value:1:${#value}-2}"
|
||||
fi
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
env_value_from_file() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
[[ -f "$file" ]] || return 0
|
||||
local line
|
||||
line="$(grep -E "^(export[[:space:]]+)?${key}=" "$file" | tail -n 1 || true)"
|
||||
[[ -n "$line" ]] || return 0
|
||||
line="${line#export }"
|
||||
local value="${line#*=}"
|
||||
strip_quotes "$value"
|
||||
}
|
||||
|
||||
resolve_abs_path() {
|
||||
local p="$1"
|
||||
python3 - "$p" <<'PY'
|
||||
import os
|
||||
import sys
|
||||
|
||||
path = sys.argv[1]
|
||||
print(os.path.abspath(os.path.expanduser(path)))
|
||||
PY
|
||||
}
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
REPO_ROOT="$ROOT_DIR"
|
||||
ENV_FILE="$ROOT_DIR/.env"
|
||||
CONFIG_DIR=""
|
||||
WORKSPACE_DIR=""
|
||||
OUTPUT_DIR="$ROOT_DIR/backups"
|
||||
BACKUP_NAME=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--repo-root)
|
||||
REPO_ROOT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--env-file)
|
||||
ENV_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--config-dir)
|
||||
CONFIG_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--workspace-dir)
|
||||
WORKSPACE_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--output-dir)
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--name)
|
||||
BACKUP_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
fail "Unknown argument: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
require_cmd tar
|
||||
require_cmd rsync
|
||||
require_cmd shasum
|
||||
require_cmd python3
|
||||
require_cmd date
|
||||
require_cmd uname
|
||||
|
||||
REPO_ROOT="$(resolve_abs_path "$REPO_ROOT")"
|
||||
ENV_FILE="$(resolve_abs_path "$ENV_FILE")"
|
||||
OUTPUT_DIR="$(resolve_abs_path "$OUTPUT_DIR")"
|
||||
|
||||
if [[ -z "$CONFIG_DIR" ]]; then
|
||||
CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$(env_value_from_file "$ENV_FILE" OPENCLAW_CONFIG_DIR)}"
|
||||
fi
|
||||
if [[ -z "$WORKSPACE_DIR" ]]; then
|
||||
WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$(env_value_from_file "$ENV_FILE" OPENCLAW_WORKSPACE_DIR)}"
|
||||
fi
|
||||
|
||||
CONFIG_DIR="${CONFIG_DIR:-$HOME/.openclaw}"
|
||||
WORKSPACE_DIR="${WORKSPACE_DIR:-$HOME/.openclaw/workspace}"
|
||||
CONFIG_DIR="$(resolve_abs_path "$CONFIG_DIR")"
|
||||
WORKSPACE_DIR="$(resolve_abs_path "$WORKSPACE_DIR")"
|
||||
|
||||
[[ -d "$CONFIG_DIR" ]] || fail "Config directory does not exist: $CONFIG_DIR"
|
||||
[[ -d "$WORKSPACE_DIR" ]] || fail "Workspace directory does not exist: $WORKSPACE_DIR"
|
||||
[[ -d "$REPO_ROOT" ]] || fail "Repo root does not exist: $REPO_ROOT"
|
||||
|
||||
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
BACKUP_NAME="${BACKUP_NAME:-openclaw-backup-${timestamp}}"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
tmpdir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
stage="$tmpdir/stage"
|
||||
mkdir -p "$stage/payload/config" "$stage/payload/workspace" "$stage/payload/repo" "$stage/meta"
|
||||
|
||||
echo "==> Copying config directory"
|
||||
rsync -a "$CONFIG_DIR/" "$stage/payload/config/"
|
||||
|
||||
echo "==> Copying workspace directory"
|
||||
rsync -a "$WORKSPACE_DIR/" "$stage/payload/workspace/"
|
||||
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
echo "==> Including env file: $ENV_FILE"
|
||||
cp "$ENV_FILE" "$stage/payload/repo/.env"
|
||||
fi
|
||||
|
||||
for file in docker-compose.yml docker-compose.extra.yml Dockerfile docker-setup.sh; do
|
||||
if [[ -f "$REPO_ROOT/$file" ]]; then
|
||||
cp "$REPO_ROOT/$file" "$stage/payload/repo/$file"
|
||||
fi
|
||||
done
|
||||
|
||||
{
|
||||
echo "timestamp_utc=$timestamp"
|
||||
echo "source_host=$(hostname -s || hostname)"
|
||||
echo "source_arch=$(uname -m)"
|
||||
echo "source_os=$(uname -s)"
|
||||
echo "repo_root=$REPO_ROOT"
|
||||
echo "config_dir=$CONFIG_DIR"
|
||||
echo "workspace_dir=$WORKSPACE_DIR"
|
||||
} >"$stage/meta/backup.env"
|
||||
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
{
|
||||
echo "# docker version"
|
||||
docker version --format '{{.Server.Version}}' 2>/dev/null || true
|
||||
echo
|
||||
echo "# docker compose ps"
|
||||
docker compose -f "$REPO_ROOT/docker-compose.yml" ps 2>/dev/null || true
|
||||
} >"$stage/meta/docker.txt"
|
||||
fi
|
||||
|
||||
if command -v git >/dev/null 2>&1 && [[ -d "$REPO_ROOT/.git" ]]; then
|
||||
{
|
||||
echo "branch=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)"
|
||||
echo "commit=$(git -C "$REPO_ROOT" rev-parse HEAD)"
|
||||
echo
|
||||
echo "# status"
|
||||
git -C "$REPO_ROOT" status --short
|
||||
} >"$stage/meta/git.txt"
|
||||
fi
|
||||
|
||||
(
|
||||
cd "$stage"
|
||||
find . -type f ! -name SHA256SUMS -print0 | sort -z | xargs -0 shasum -a 256 > SHA256SUMS
|
||||
)
|
||||
|
||||
archive_path="$OUTPUT_DIR/${BACKUP_NAME}.tar.gz"
|
||||
(
|
||||
cd "$stage"
|
||||
tar -czf "$archive_path" .
|
||||
)
|
||||
shasum -a 256 "$archive_path" > "${archive_path}.sha256"
|
||||
|
||||
echo
|
||||
echo "Backup created:"
|
||||
echo " $archive_path"
|
||||
echo " ${archive_path}.sha256"
|
||||
echo
|
||||
echo "Next step on target host:"
|
||||
echo " scripts/migrate/restore-openclaw.sh --archive \"$archive_path\""
|
||||
201
scripts/migrate/restore-openclaw.sh
Executable file
201
scripts/migrate/restore-openclaw.sh
Executable file
@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/migrate/restore-openclaw.sh --archive <path> [options]
|
||||
|
||||
Options:
|
||||
--archive <path> Backup archive created by backup-openclaw.sh (required)
|
||||
--repo-root <path> OpenClaw repo root (default: current repo)
|
||||
--env-file <path> Env file path (default: <repo-root>/.env)
|
||||
--config-dir <path> OpenClaw config dir (default: env or ~/.openclaw)
|
||||
--workspace-dir <path> OpenClaw workspace dir (default: env or ~/.openclaw/workspace)
|
||||
--apply-env Overwrite --env-file with backup .env (default: false)
|
||||
--no-stop Do not stop gateway container before restore
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "Missing required command: $1"
|
||||
}
|
||||
|
||||
strip_quotes() {
|
||||
local value="$1"
|
||||
if [[ "${value}" == \"*\" && "${value}" == *\" ]]; then
|
||||
value="${value:1:${#value}-2}"
|
||||
elif [[ "${value}" == \'*\' && "${value}" == *\' ]]; then
|
||||
value="${value:1:${#value}-2}"
|
||||
fi
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
env_value_from_file() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
[[ -f "$file" ]] || return 0
|
||||
local line
|
||||
line="$(grep -E "^(export[[:space:]]+)?${key}=" "$file" | tail -n 1 || true)"
|
||||
[[ -n "$line" ]] || return 0
|
||||
line="${line#export }"
|
||||
local value="${line#*=}"
|
||||
strip_quotes "$value"
|
||||
}
|
||||
|
||||
resolve_abs_path() {
|
||||
local p="$1"
|
||||
python3 - "$p" <<'PY'
|
||||
import os
|
||||
import sys
|
||||
|
||||
path = sys.argv[1]
|
||||
print(os.path.abspath(os.path.expanduser(path)))
|
||||
PY
|
||||
}
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
REPO_ROOT="$ROOT_DIR"
|
||||
ENV_FILE="$ROOT_DIR/.env"
|
||||
ARCHIVE_PATH=""
|
||||
CONFIG_DIR=""
|
||||
WORKSPACE_DIR=""
|
||||
APPLY_ENV=0
|
||||
STOP_FIRST=1
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--archive)
|
||||
ARCHIVE_PATH="$2"
|
||||
shift 2
|
||||
;;
|
||||
--repo-root)
|
||||
REPO_ROOT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--env-file)
|
||||
ENV_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--config-dir)
|
||||
CONFIG_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--workspace-dir)
|
||||
WORKSPACE_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--apply-env)
|
||||
APPLY_ENV=1
|
||||
shift
|
||||
;;
|
||||
--no-stop)
|
||||
STOP_FIRST=0
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
fail "Unknown argument: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$ARCHIVE_PATH" ]] || fail "--archive is required"
|
||||
|
||||
require_cmd tar
|
||||
require_cmd rsync
|
||||
require_cmd shasum
|
||||
require_cmd python3
|
||||
require_cmd date
|
||||
|
||||
ARCHIVE_PATH="$(resolve_abs_path "$ARCHIVE_PATH")"
|
||||
REPO_ROOT="$(resolve_abs_path "$REPO_ROOT")"
|
||||
ENV_FILE="$(resolve_abs_path "$ENV_FILE")"
|
||||
|
||||
[[ -f "$ARCHIVE_PATH" ]] || fail "Archive not found: $ARCHIVE_PATH"
|
||||
[[ -d "$REPO_ROOT" ]] || fail "Repo root does not exist: $REPO_ROOT"
|
||||
|
||||
tmpdir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
echo "==> Extracting archive"
|
||||
tar -xzf "$ARCHIVE_PATH" -C "$tmpdir"
|
||||
|
||||
[[ -f "$tmpdir/SHA256SUMS" ]] || fail "Archive missing SHA256SUMS"
|
||||
(
|
||||
cd "$tmpdir"
|
||||
shasum -a 256 -c SHA256SUMS
|
||||
)
|
||||
|
||||
if [[ -z "$CONFIG_DIR" ]]; then
|
||||
CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$(env_value_from_file "$ENV_FILE" OPENCLAW_CONFIG_DIR)}"
|
||||
fi
|
||||
if [[ -z "$WORKSPACE_DIR" ]]; then
|
||||
WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$(env_value_from_file "$ENV_FILE" OPENCLAW_WORKSPACE_DIR)}"
|
||||
fi
|
||||
|
||||
CONFIG_DIR="${CONFIG_DIR:-$HOME/.openclaw}"
|
||||
WORKSPACE_DIR="${WORKSPACE_DIR:-$HOME/.openclaw/workspace}"
|
||||
CONFIG_DIR="$(resolve_abs_path "$CONFIG_DIR")"
|
||||
WORKSPACE_DIR="$(resolve_abs_path "$WORKSPACE_DIR")"
|
||||
|
||||
if [[ $STOP_FIRST -eq 1 ]] && command -v docker >/dev/null 2>&1; then
|
||||
echo "==> Stopping gateway container"
|
||||
docker compose -f "$REPO_ROOT/docker-compose.yml" stop openclaw-gateway >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
mkdir -p "$(dirname "$CONFIG_DIR")" "$(dirname "$WORKSPACE_DIR")"
|
||||
|
||||
if [[ -d "$CONFIG_DIR" ]]; then
|
||||
mv "$CONFIG_DIR" "${CONFIG_DIR}.pre-restore-${timestamp}"
|
||||
fi
|
||||
if [[ -d "$WORKSPACE_DIR" ]]; then
|
||||
mv "$WORKSPACE_DIR" "${WORKSPACE_DIR}.pre-restore-${timestamp}"
|
||||
fi
|
||||
|
||||
mkdir -p "$CONFIG_DIR" "$WORKSPACE_DIR"
|
||||
|
||||
echo "==> Restoring config"
|
||||
rsync -a "$tmpdir/payload/config/" "$CONFIG_DIR/"
|
||||
|
||||
echo "==> Restoring workspace"
|
||||
rsync -a "$tmpdir/payload/workspace/" "$WORKSPACE_DIR/"
|
||||
|
||||
if [[ -f "$tmpdir/payload/repo/.env" ]]; then
|
||||
if [[ $APPLY_ENV -eq 1 ]]; then
|
||||
mkdir -p "$(dirname "$ENV_FILE")"
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
cp "$ENV_FILE" "${ENV_FILE}.pre-restore-${timestamp}"
|
||||
fi
|
||||
cp "$tmpdir/payload/repo/.env" "$ENV_FILE"
|
||||
echo "==> Applied backed up env file to $ENV_FILE"
|
||||
else
|
||||
cp "$tmpdir/payload/repo/.env" "${ENV_FILE}.from-backup"
|
||||
echo "==> Wrote env candidate to ${ENV_FILE}.from-backup"
|
||||
fi
|
||||
fi
|
||||
|
||||
source_arch="$(grep -E '^source_arch=' "$tmpdir/meta/backup.env" | cut -d= -f2- || true)"
|
||||
target_arch="$(uname -m)"
|
||||
if [[ -n "$source_arch" && "$source_arch" != "$target_arch" ]]; then
|
||||
echo
|
||||
echo "NOTE: source arch (${source_arch}) differs from target arch (${target_arch})."
|
||||
echo "Rebuild the Docker image on this host; do not reuse old binary caches or volumes."
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Restore completed."
|
||||
echo "Next steps:"
|
||||
echo " 1) docker compose -f \"$REPO_ROOT/docker-compose.yml\" up -d --build --force-recreate openclaw-gateway"
|
||||
echo " 2) docker compose -f \"$REPO_ROOT/docker-compose.yml\" run --rm openclaw-cli health"
|
||||
echo " 3) docker compose -f \"$REPO_ROOT/docker-compose.yml\" run --rm openclaw-cli channels status --probe"
|
||||
Loading…
x
Reference in New Issue
Block a user