#!/usr/bin/env bash # One-time host setup for rootless OpenClaw in Podman. Uses the current # non-root user throughout, builds or pulls the image into that user's Podman # store, writes config under ~/.openclaw by default, and uses the repo-local # launch script at ./scripts/run-openclaw-podman.sh. # # Usage: ./scripts/podman/setup.sh [--quadlet|--container] # --quadlet Install a Podman Quadlet as the current user's systemd service # --container Only install image + config; you start the container manually (default) # Or set OPENCLAW_PODMAN_QUADLET=1 (or 0) to choose without a flag. # # After this, start the gateway manually: # ./scripts/run-openclaw-podman.sh launch # ./scripts/run-openclaw-podman.sh launch setup # Or, if you used --quadlet: # systemctl --user start openclaw.service
set -euo pipefail
require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then echo"Missing dependency: $1" >&2
exit 1 fi
}
is_root() { [[ "$(id -u)" -eq 0 ]]; }
fail() { echo"$*" >&2
exit 1
}
validate_single_line_value() {
local label="$1"
local value="$2" if [[ "$value" == *$'\n'* || "$value" == *$'\r'* ]]; then
fail "Invalid $label: control characters are not allowed." fi
}
validate_absolute_path() {
local label="$1"
local value="$2"
validate_single_line_value "$label""$value"
[[ "$value" == /* ]] || fail "Invalid $label: expected an absolute path."
[[ "$value" != *"//"* ]] || fail "Invalid $label: repeated slashes are not allowed."
[[ "$value" != *"/./"* && "$value" != */. && "$value" != *"/../"* && "$value" != */.. ]] ||
fail "Invalid $label: dot path segments are not allowed."
}
validate_mount_source_path() {
local label="$1"
local value="$2"
validate_absolute_path "$label""$value"
[[ "$value" != *:* ]] || fail "Invalid $label: ':' is not allowed in Podman bind-mount source paths."
}
validate_image_name() {
local value="$1"
validate_single_line_value "image name""$value"
case "$value" in
oci-archive:*|docker-archive:*|dir:*|oci:*|containers-storage:*|docker-daemon:*|archive:* )
fail "Invalid image name: transport prefixes are not allowed: $value"
;;
esac
[[ "$value" =~ ^[A-Za-z0-9][A-Za-z0-9._/:@-]*$ ]] ||
fail "Invalid image name: $value"
}
ensure_safe_existing_dir() {
local label="$1"
local dir="$2"
validate_absolute_path "$label""$dir"
[[ -d "$dir" ]] || fail "Missing $label: $dir"
[[ ! -L "$dir" ]] || fail "Unsafe $label: symlinks are not allowed ($dir)"
}
stat_uid() {
local path="$1" if stat -f '%u'"$path" >/dev/null 2>&1; then
stat -f '%u'"$path" else
stat -Lc '%u'"$path" fi
}
stat_mode() {
local path="$1" if stat -f '%Lp'"$path" >/dev/null 2>&1; then
stat -f '%Lp'"$path" else
stat -Lc '%a'"$path" fi
}
ensure_private_existing_dir_owned_by_user() {
local label="$1"
local dir="$2"
local uid=""
local mode=""
ensure_safe_existing_dir "$label""$dir"
uid="$(stat_uid "$dir")"
[[ "$uid" == "$(id -u)" ]] || fail "Unsafe $label: not owned by current user ($dir)"
mode="$(stat_mode "$dir")"
(( (8#$mode & 0022) == 0 )) || fail "Unsafe $label: group/other writable ($dir)"
}
ensure_safe_write_file_path() {
local label="$1"
local file="$2"
local dir
validate_absolute_path "$label""$file" if [[ -e "$file" ]]; then
[[ ! -L "$file" ]] || fail "Unsafe $label: symlinks are not allowed ($file)"
[[ -f "$file" ]] || fail "Unsafe $label: expected a regular file ($file)" fi
dir="$(dirname "$file")"
ensure_safe_existing_dir "${label} parent directory""$dir"
}
write_file_atomically() {
local file="$1"
local mode="$2"
local dir=""
local tmp=""
ensure_safe_write_file_path "output file""$file"
dir="$(dirname "$file")"
tmp="$(mktemp "$dir/.tmp.XXXXXX")" cat >"$tmp"
chmod "$mode""$tmp"
mv -f "$tmp""$file"
}
validate_port() {
local label="$1"
local value="$2"
local numeric=""
[[ "$value" =~ ^[0-9]{1,5}$ ]] || fail "Invalid $label: must be numeric."
numeric=$((10#$value))
(( numeric >= 1 && numeric <= 65535 )) || fail "Invalid $label: out of range."
}
escape_sed_replacement_pipe_delim() {
printf '%s'"$1" | sed -e 's/[\\&|]/\\&/g'
}
resolve_user_home() {
local user="$1"
local home="" if command -v getent >/dev/null 2>&1; then
home="$(getent passwd "$user" 2>/dev/null | cut -d: -f6 || true)" fi if [[ -z "$home" && -f /etc/passwd ]]; then
home="$(awk -F: -v u="$user" '$1==u {print $6}' /etc/passwd 2>/dev/null || true)" fi if [[ -z "$home" ]]; then
home="/home/$user" fi
printf '%s'"$home"
}
generate_token_hex_32() { if command -v openssl >/dev/null 2>&1; then
openssl rand -hex 32
return 0 fi if command -v python3 >/dev/null 2>&1; then
python3 - <<'PY' import secrets
print(secrets.token_hex(32))
PY
return 0 fi if command -v od >/dev/null 2>&1; then
od -An -N32 -tx1 /dev/urandom | tr -d " \n"
return 0 fi echo"Missing dependency: need openssl or python3 (or od) to generate OPENCLAW_GATEWAY_TOKEN." >&2
exit 1
}
seed_local_control_ui_origins() {
local file="$1"
local port="$2"
local dir=""
local tmp=""
ensure_safe_write_file_path "config file""$file" if ! command -v python3 >/dev/null 2>&1; then echo"Warning: python3 not found; unable to seed gateway.controlUi.allowedOrigins in $file." >&2
return 0 fi
dir="$(dirname "$file")"
tmp="$(mktemp "$dir/.config.tmp.XXXXXX")" if ! python3 - "$file""$port""$tmp" <<'PY' import json import sys
path = sys.argv[1]
port = sys.argv[2]
tmp = sys.argv[3]
try:
with open(path, "r", encoding="utf-8") as fh:
data = json.load(fh)
except json.JSONDecodeError as exc:
print(
f"Warning: unable to seed gateway.controlUi.allowedOrigins in {path}: existing config is not strict JSON ({exc}). Leaving file unchanged.",
file=sys.stderr,
)
raise SystemExit(1) if not isinstance(data, dict):
raise SystemExit(f"{path}: expected top-level object")
gateway = data.setdefault("gateway", {}) if not isinstance(gateway, dict):
raise SystemExit(f"{path}: expected gateway object")
gateway.setdefault("mode", "local")
control_ui = gateway.setdefault("controlUi", {}) if not isinstance(control_ui, dict):
raise SystemExit(f"{path}: expected gateway.controlUi object")
allowed = control_ui.get("allowedOrigins")
managed_localhosts = {"127.0.0.1", "localhost"}
desired = [
f"http://127.0.0.1:{port}",
f"http://localhost:{port}",
] if not isinstance(allowed, list):
allowed = []
cleaned = [] for origin in allowed: if not isinstance(origin, str):
continue
normalized = origin.strip() if not normalized:
continue if normalized.startswith("http://"):
host_port = normalized[len("http://") :]
host = host_port.split(":", 1)[0] if host in managed_localhosts:
continue
cleaned.append(normalized)
control_ui["allowedOrigins"] = cleaned + desired
with open(tmp, "w", encoding="utf-8") as fh:
json.dump(data, fh, indent=2)
fh.write("\n")
PY then rm -f "$tmp"
return 0 fi
[[ -s "$tmp" ]] || { rm -f "$tmp"
return 0
}
chmod 600"$tmp"2>/dev/null || true
mv -f "$tmp""$file"
}
upsert_env_var() {
local file="$1"
local key="$2"
local value="$3"
local tmp
local dir
ensure_safe_write_file_path "env file""$file"
dir="$(dirname "$file")"
tmp="$(mktemp "$dir/.env.tmp.XXXXXX")" if [[ -f "$file" ]]; then
awk -v k="$key" -v v="$value"' BEGIN { found = 0 }
$0 ~ ("^" k "=") { print k "=" v; found = 1; next }
{ print } END { if (!found) print k "=" v } ' "$file" >"$tmp" else
printf '%s=%s\n'"$key""$value" >"$tmp" fi
mv "$tmp""$file"
chmod 600"$file"2>/dev/null || true
}
INSTALL_QUADLET=false for arg in "$@"; do
case "$arg" in
--quadlet) INSTALL_QUADLET=true ;;
--container) INSTALL_QUADLET=false ;;
esac done if [[ -n "${OPENCLAW_PODMAN_QUADLET:-}" ]]; then
case "${OPENCLAW_PODMAN_QUADLET,,}" in 1|yes|true) INSTALL_QUADLET=true ;; 0|no|false) INSTALL_QUADLET=false ;;
esac fi if [[ "$INSTALL_QUADLET" == true && "$PLATFORM_NAME" != "Linux" ]]; then
fail "--quadlet is only supported on Linux with systemd user services." fi
SEED_GATEWAY_PORT="$HOST_GATEWAY_PORT" if [[ "$INSTALL_QUADLET" == true ]]; then
SEED_GATEWAY_PORT="$QUADLET_GATEWAY_PORT" fi
require_cmd podman if is_root; then echo"Run scripts/podman/setup.sh as your normal user so Podman stays rootless." >&='color: green'>2
exit 1 fi if [[ "$OPENCLAW_IMAGE" == "openclaw:local" ]] && [[ ! -f "$REPO_PATH/Dockerfile" ]]; then echo"Dockerfile not found at $REPO_PATH. Set OPENCLAW_REPO_PATH to the repo root." >&le='color: green'>2
exit 1 fi if [[ ! -f "$RUN_SCRIPT_SRC" ]]; then echo"Launch script not found at $RUN_SCRIPT_SRC." >&2
exit 1 fi
if [[ -z "$OPENCLAW_HOME" ]]; then
OPENCLAW_HOME="$(resolve_user_home "$OPENCLAW_USER")" fi if [[ -z "$OPENCLAW_HOME" ]]; then echo"Unable to resolve HOME for user $OPENCLAW_USER." >&2
exit 1 fi if [[ -z "$OPENCLAW_CONFIG_DIR" ]]; then
OPENCLAW_CONFIG_DIR="$OPENCLAW_HOME/.openclaw" fi if [[ -z "$OPENCLAW_WORKSPACE_DIR" ]]; then
OPENCLAW_WORKSPACE_DIR="$OPENCLAW_CONFIG_DIR/workspace" fi
validate_absolute_path "home directory""$OPENCLAW_HOME"
validate_mount_source_path "config directory""$OPENCLAW_CONFIG_DIR"
validate_mount_source_path "workspace directory""$OPENCLAW_WORKSPACE_DIR"
validate_container_name "$OPENCLAW_CONTAINER_NAME"
validate_image_name "$OPENCLAW_IMAGE"
validate_port "gateway host port""$HOST_GATEWAY_PORT"
validate_port "seed gateway port""$SEED_GATEWAY_PORT"
BUILD_ARGS=() if [[ -n "${OPENCLAW_DOCKER_APT_PACKAGES:-}" ]]; then
BUILD_ARGS+=(--build-arg "OPENCLAW_DOCKER_APT_PACKAGES=${OPENCLAW_DOCKER_APT_PACKAGES}") fi if [[ -n "${OPENCLAW_EXTENSIONS:-}" ]]; then
BUILD_ARGS+=(--build-arg "OPENCLAW_EXTENSIONS=${OPENCLAW_EXTENSIONS}") fi
if [[ "$OPENCLAW_IMAGE" == "openclaw:local" ]]; then echo"Building image $OPENCLAW_IMAGE ..."
podman build -t "$OPENCLAW_IMAGE" -f "$REPO_PATH/Dockerfile""${BUILD_ARGS[@]+"${BUILD_ARGS[@]}"}""$REPO_PATH" else if podman image exists "$OPENCLAW_IMAGE" >/dev/null 2>&1; then echo"Using existing image $OPENCLAW_IMAGE" else echo"Pulling image $OPENCLAW_IMAGE ..."
podman pull "$OPENCLAW_IMAGE" fi fi
ENV_FILE="$OPENCLAW_CONFIG_DIR/.env" if [[ ! -f "$ENV_FILE" ]]; then
TOKEN="$(generate_token_hex_32)"
(
umask 077
write_file_atomically "$ENV_FILE"600 <<EOF
OPENCLAW_GATEWAY_TOKEN=$TOKEN
EOF
) echo"Generated OPENCLAW_GATEWAY_TOKEN and wrote it to $ENV_FILE" fi
upsert_env_var "$ENV_FILE""OPENCLAW_PODMAN_CONTAINER""$OPENCLAW_CONTAINER_NAME"
upsert_env_var "$ENV_FILE""OPENCLAW_PODMAN_IMAGE""$OPENCLAW_IMAGE"
CONFIG_JSON="$OPENCLAW_CONFIG_DIR/openclaw.json" if [[ ! -f "$CONFIG_JSON" ]]; then
(
umask 077
write_file_atomically "$CONFIG_JSON"600 <<JSON
{ "gateway": { "mode": "local", "controlUi": { "allowedOrigins": [ "http://127.0.0.1:${SEED_GATEWAY_PORT}", "http://localhost:${SEED_GATEWAY_PORT}"
]
}
}
}
JSON
) echo"Wrote minimal config to $CONFIG_JSON" fi
seed_local_control_ui_origins "$CONFIG_JSON""$SEED_GATEWAY_PORT"
if command -v systemctl >/dev/null 2>&1; then echo"Reloading and starting user service..." if systemctl --user daemon-reload && systemctl --user start openclaw.service; then echo"Quadlet installed and service started." else echo"Quadlet installed, but automatic start failed." >&2 echo"Try: systemctl --user daemon-reload && systemctl --user start openclaw.service" >&tyle='color: green'>2 if command -v loginctl >/dev/null 2>&1; then echo"For boot persistence on headless hosts, you may also need: sudo loginctl enable-linger $(whoami)" >&2 fi fi else echo"systemctl not found; Quadlet installed but not started." >&2 fi else echo"Container setup complete." fi
¤ Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.0.14Bemerkung:
(vorverarbeitet am 2026-06-06)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.