From 81b5e2766bdcb4dbcc72a881c1d950627e955ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Sp=C3=B6rk?= Date: Sat, 14 Feb 2026 17:39:06 +0100 Subject: [PATCH] feat(podman): add optional Podman setup and documentation (#16273) * feat(podman): add optional Podman setup and documentation - Introduced `setup-podman.sh` for one-time host setup of OpenClaw in a rootless Podman environment, including user creation, image building, and launch script installation. - Added `run-openclaw-podman.sh` for running the OpenClaw gateway as a Podman container. - Created `openclaw.podman.env` for environment variable configuration. - Updated documentation to include Podman installation instructions and a new dedicated Podman guide. - Added a systemd Quadlet unit for managing the OpenClaw service as a user service. * fix: harden Podman setup and docs (#16273) (thanks @DarwinsBuddy) * style: format cli credentials --------- Co-authored-by: Peter Steinberger --- CHANGELOG.md | 1 + docs/docs.json | 12 +- docs/install/index.md | 3 + docs/install/podman.md | 105 +++++++++++++ openclaw.podman.env | 24 +++ scripts/podman/openclaw.container.in | 26 ++++ scripts/run-openclaw-podman.sh | 189 +++++++++++++++++++++++ setup-podman.sh | 215 +++++++++++++++++++++++++++ 8 files changed, 574 insertions(+), 1 deletion(-) create mode 100644 docs/install/podman.md create mode 100644 openclaw.podman.env create mode 100644 scripts/podman/openclaw.container.in create mode 100755 scripts/run-openclaw-podman.sh create mode 100755 setup-podman.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index e803b1a260..cb8c0ac1db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Install: add optional Podman-based setup: `setup-podman.sh` for one-time host setup (openclaw user, image, launch script, systemd quadlet), `run-openclaw-podman.sh launch` / `launch setup`; systemd Quadlet unit for openclaw user service; docs for rootless container, openclaw user (subuid/subgid), and quadlet (troubleshooting). (#16273) Thanks @DarwinsBuddy. - Discord: send voice messages with waveform previews from local audio files (including silent delivery). (#7253) Thanks @nyanjou. - Discord: add configurable presence status/activity/type/url (custom status defaults to activity text). (#10855) Thanks @h0tp-ftw. - Slack/Plugins: add thread-ownership outbound gating via `message_sending` hooks, including @-mention bypass tracking and Slack outbound hook wiring for cancel/modify behavior. (#15775) Thanks @DarlingtonDeveloper. diff --git a/docs/docs.json b/docs/docs.json index f8f81e6e13..0952953b0a 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -319,6 +319,10 @@ "source": "/docker", "destination": "/install/docker" }, + { + "source": "/podman", + "destination": "/install/podman" + }, { "source": "/doctor", "destination": "/gateway/doctor" @@ -836,7 +840,13 @@ }, { "group": "Other install methods", - "pages": ["install/docker", "install/nix", "install/ansible", "install/bun"] + "pages": [ + "install/docker", + "install/podman", + "install/nix", + "install/ansible", + "install/bun" + ] }, { "group": "Maintenance", diff --git a/docs/install/index.md b/docs/install/index.md index a1e966c02c..f9da04d71a 100644 --- a/docs/install/index.md +++ b/docs/install/index.md @@ -142,6 +142,9 @@ The **installer script** is the recommended way to install OpenClaw. It handles Containerized or headless deployments. + + Rootless container: run `setup-podman.sh` once, then the launch script. + Declarative install via Nix. diff --git a/docs/install/podman.md b/docs/install/podman.md new file mode 100644 index 0000000000..2bf8b61ac2 --- /dev/null +++ b/docs/install/podman.md @@ -0,0 +1,105 @@ +--- +summary: "Run OpenClaw in a rootless Podman container" +read_when: + - You want a containerized gateway with Podman instead of Docker +title: "Podman" +--- + +# Podman + +Run the OpenClaw gateway in a **rootless** Podman container. Uses the same image as Docker (build from the repo [Dockerfile](https://github.com/openclaw/openclaw/blob/main/Dockerfile)). + +## Requirements + +- Podman (rootless) +- Sudo for one-time setup (create user, build image) + +## Quick start + +**1. One-time setup** (from repo root; creates user, builds image, installs launch script): + +```bash +./setup-podman.sh +``` + +By default the container is **not** installed as a systemd service, you start it manually (see below). For a production-style setup with auto-start and restarts, install it as a systemd Quadlet user service instead: + +```bash +./setup-podman.sh --quadlet +``` + +(Or set `OPENCLAW_PODMAN_QUADLET=1`; use `--container` to install only the container and launch script.) + +**2. Start gateway** (manual, for quick smoke testing): + +```bash +./scripts/run-openclaw-podman.sh launch +``` + +**3. Onboarding wizard** (e.g. to add channels or providers): + +```bash +./scripts/run-openclaw-podman.sh launch setup +``` + +Then open `http://127.0.0.1:18789/` and use the token from `~openclaw/.openclaw/.env` (or the value printed by setup). + +## Systemd (Quadlet, optional) + +If you ran `./setup-podman.sh --quadlet` (or `OPENCLAW_PODMAN_QUADLET=1`), a [Podman Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) unit is installed so the gateway runs as a systemd user service for the openclaw user. The service is enabled and started at the end of setup. + +- **Start:** `sudo systemctl --machine openclaw@ --user start openclaw.service` +- **Stop:** `sudo systemctl --machine openclaw@ --user stop openclaw.service` +- **Status:** `sudo systemctl --machine openclaw@ --user status openclaw.service` +- **Logs:** `sudo journalctl --machine openclaw@ --user -u openclaw.service -f` + +The quadlet file lives at `~openclaw/.config/containers/systemd/openclaw.container`. To change ports or env, edit that file (or the `.env` it sources), then `sudo systemctl --machine openclaw@ --user daemon-reload` and restart the service. On boot, the service starts automatically if lingering is enabled for openclaw (setup does this when loginctl is available). + +To add quadlet **after** an initial setup that did not use it, re-run: `./setup-podman.sh --quadlet`. + +## The openclaw user (non-login) + +`setup-podman.sh` creates a dedicated system user `openclaw`: + +- **Shell:** `nologin` — no interactive login; reduces attack surface. +- **Home:** e.g. `/home/openclaw` — holds `~/.openclaw` (config, workspace) and the launch script `run-openclaw-podman.sh`. +- **Rootless Podman:** The user must have a **subuid** and **subgid** range. Many distros assign these automatically when the user is created. If setup prints a warning, add lines to `/etc/subuid` and `/etc/subgid`: + + ```text + openclaw:100000:65536 + ``` + + Then start the gateway as that user (e.g. from cron or systemd): + + ```bash + sudo -u openclaw /home/openclaw/run-openclaw-podman.sh + sudo -u openclaw /home/openclaw/run-openclaw-podman.sh setup + ``` + +- **Config:** Only `openclaw` and root can access `/home/openclaw/.openclaw`. To edit config: use the Control UI once the gateway is running, or `sudo -u openclaw $EDITOR /home/openclaw/.openclaw/openclaw.json`. + +## Environment and config + +- **Token:** Stored in `~openclaw/.openclaw/.env` as `OPENCLAW_GATEWAY_TOKEN`. Generate with: `openssl rand -hex 32`. +- **Optional:** In that `.env` you can set provider keys (e.g. `GROQ_API_KEY`, `OLLAMA_API_KEY`) and other OpenClaw env vars. +- **Host ports:** By default the script maps `18789` (gateway) and `18790` (bridge). Override the **host** port mapping with `OPENCLAW_PODMAN_GATEWAY_HOST_PORT` and `OPENCLAW_PODMAN_BRIDGE_HOST_PORT` when launching. +- **Paths:** Host config and workspace default to `~openclaw/.openclaw` and `~openclaw/.openclaw/workspace`. Override the host paths used by the launch script with `OPENCLAW_CONFIG_DIR` and `OPENCLAW_WORKSPACE_DIR`. + +## Useful commands + +- **Logs:** With quadlet: `sudo journalctl --machine openclaw@ --user -u openclaw.service -f`. With script: `sudo -u openclaw podman logs -f openclaw` +- **Stop:** With quadlet: `sudo systemctl --machine openclaw@ --user stop openclaw.service`. With script: `sudo -u openclaw podman stop openclaw` +- **Start again:** With quadlet: `sudo systemctl --machine openclaw@ --user start openclaw.service`. With script: re-run the launch script or `podman start openclaw` +- **Remove container:** `sudo -u openclaw podman rm -f openclaw` — config and workspace on the host are kept + +## Troubleshooting + +- **Permission denied (EACCES) on config or auth-profiles:** The container defaults to `--userns=keep-id` and runs as the same uid/gid as the host user running the script. Ensure your host `OPENCLAW_CONFIG_DIR` and `OPENCLAW_WORKSPACE_DIR` are owned by that user. +- **Rootless Podman fails for user openclaw:** Check `/etc/subuid` and `/etc/subgid` contain a line for `openclaw` (e.g. `openclaw:100000:65536`). Add it if missing and restart. +- **Container name in use:** The launch script uses `podman run --replace`, so the existing container is replaced when you start again. To clean up manually: `podman rm -f openclaw`. +- **Script not found when running as openclaw:** Ensure `setup-podman.sh` was run so that `run-openclaw-podman.sh` is copied to openclaw’s home (e.g. `/home/openclaw/run-openclaw-podman.sh`). +- **Quadlet service not found or fails to start:** Run `sudo systemctl --machine openclaw@ --user daemon-reload` after editing the `.container` file. Quadlet requires cgroups v2: `podman info --format '{{.Host.CgroupsVersion}}'` should show `2`. + +## Optional: run as your own user + +To run the gateway as your normal user (no dedicated openclaw user): build the image, create `~/.openclaw/.env` with `OPENCLAW_GATEWAY_TOKEN`, and run the container with `--userns=keep-id` and mounts to your `~/.openclaw`. The launch script is designed for the openclaw-user flow; for a single-user setup you can instead run the `podman run` command from the script manually, pointing config and workspace to your home. Recommended for most users: use `setup-podman.sh` and run as the openclaw user so config and process are isolated. diff --git a/openclaw.podman.env b/openclaw.podman.env new file mode 100644 index 0000000000..34500ab809 --- /dev/null +++ b/openclaw.podman.env @@ -0,0 +1,24 @@ +# OpenClaw Podman environment +# Copy to openclaw.podman.env.local and set OPENCLAW_GATEWAY_TOKEN (or use -e when running). +# This file can be used with: +# OPENCLAW_PODMAN_ENV=/path/to/openclaw.podman.env ./scripts/run-openclaw-podman.sh launch + +# Required: gateway auth token. Generate with: openssl rand -hex 32 +# Set this before running the container (or use run-openclaw-podman.sh which can generate it). +OPENCLAW_GATEWAY_TOKEN= + +# Optional: web provider (leave empty to skip) +# CLAUDE_AI_SESSION_KEY= +# CLAUDE_WEB_SESSION_KEY= +# CLAUDE_WEB_COOKIE= + +# Host port mapping (defaults; override if needed) +OPENCLAW_PODMAN_GATEWAY_HOST_PORT=18789 +OPENCLAW_PODMAN_BRIDGE_HOST_PORT=18790 + +# Gateway bind (used by the launch script) +OPENCLAW_GATEWAY_BIND=lan + +# Optional: LLM provider API keys (for zero cost use Ollama locally or Groq free tier) +# OLLAMA_API_KEY=ollama-local +# GROQ_API_KEY= diff --git a/scripts/podman/openclaw.container.in b/scripts/podman/openclaw.container.in new file mode 100644 index 0000000000..2c9af017c2 --- /dev/null +++ b/scripts/podman/openclaw.container.in @@ -0,0 +1,26 @@ +# OpenClaw gateway — Podman Quadlet (rootless) +# Installed by setup-podman.sh into openclaw's ~/.config/containers/systemd/ +# {{OPENCLAW_HOME}} is replaced at install time. + +[Unit] +Description=OpenClaw gateway (rootless Podman) + +[Container] +Image=openclaw:local +ContainerName=openclaw +UserNS=keep-id +Volume={{OPENCLAW_HOME}}/.openclaw:/home/node/.openclaw +EnvironmentFile={{OPENCLAW_HOME}}/.openclaw/.env +Environment=HOME=/home/node +Environment=TERM=xterm-256color +PublishPort=18789:18789 +PublishPort=18790:18790 +Pull=never +Exec=node dist/index.js gateway --bind lan --port 18789 + +[Service] +TimeoutStartSec=300 +Restart=on-failure + +[Install] +WantedBy=default.target diff --git a/scripts/run-openclaw-podman.sh b/scripts/run-openclaw-podman.sh new file mode 100755 index 0000000000..fd7bd684fb --- /dev/null +++ b/scripts/run-openclaw-podman.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +# Rootless OpenClaw in Podman: run after one-time setup. +# +# One-time setup (from repo root): ./setup-podman.sh +# Then: +# ./scripts/run-openclaw-podman.sh launch # Start gateway +# ./scripts/run-openclaw-podman.sh launch setup # Onboarding wizard +# +# As the openclaw user (no repo needed): +# sudo -u openclaw /home/openclaw/run-openclaw-podman.sh +# sudo -u openclaw /home/openclaw/run-openclaw-podman.sh setup +# +# Legacy: "setup-host" delegates to ../setup-podman.sh + +set -euo pipefail + +OPENCLAW_USER="${OPENCLAW_PODMAN_USER:-openclaw}" + +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" +} + +OPENCLAW_HOME="$(resolve_user_home "$OPENCLAW_USER")" +OPENCLAW_UID="$(id -u "$OPENCLAW_USER" 2>/dev/null || true)" +LAUNCH_SCRIPT="$OPENCLAW_HOME/run-openclaw-podman.sh" + +# Legacy: setup-host → run setup-podman.sh +if [[ "${1:-}" == "setup-host" ]]; then + shift + REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + SETUP_PODMAN="$REPO_ROOT/setup-podman.sh" + if [[ -f "$SETUP_PODMAN" ]]; then + exec "$SETUP_PODMAN" "$@" + fi + echo "setup-podman.sh not found at $SETUP_PODMAN. Run from repo root: ./setup-podman.sh" >&2 + exit 1 +fi + +# --- Step 2: launch (from repo: re-exec as openclaw in safe cwd; from openclaw home: run container) --- +if [[ "${1:-}" == "launch" ]]; then + shift + if [[ -n "${OPENCLAW_UID:-}" && "$(id -u)" -ne "$OPENCLAW_UID" ]]; then + # Exec as openclaw with cwd=/tmp so a nologin user never inherits an invalid cwd. + exec sudo -u "$OPENCLAW_USER" env HOME="$OPENCLAW_HOME" PATH="$PATH" TERM="${TERM:-}" \ + bash -c 'cd /tmp && exec '"$LAUNCH_SCRIPT"' "$@"' _ "$@" + fi + # Already openclaw; fall through to container run (with remaining args, e.g. "setup") +fi + +# --- Container run (script in openclaw home, run as openclaw) --- +EFFECTIVE_HOME="${HOME:-}" +if [[ -n "${OPENCLAW_UID:-}" && "$(id -u)" -eq "$OPENCLAW_UID" ]]; then + EFFECTIVE_HOME="$OPENCLAW_HOME" + export HOME="$OPENCLAW_HOME" +fi +if [[ -z "${EFFECTIVE_HOME:-}" ]]; then + EFFECTIVE_HOME="${OPENCLAW_HOME:-/tmp}" +fi +CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$EFFECTIVE_HOME/.openclaw}" +ENV_FILE="${OPENCLAW_PODMAN_ENV:-$CONFIG_DIR/.env}" +WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$CONFIG_DIR/workspace}" +CONTAINER_NAME="${OPENCLAW_PODMAN_CONTAINER:-openclaw}" +OPENCLAW_IMAGE="${OPENCLAW_PODMAN_IMAGE:-openclaw:local}" +PODMAN_PULL="${OPENCLAW_PODMAN_PULL:-never}" +HOST_GATEWAY_PORT="${OPENCLAW_PODMAN_GATEWAY_HOST_PORT:-${OPENCLAW_GATEWAY_PORT:-18789}}" +HOST_BRIDGE_PORT="${OPENCLAW_PODMAN_BRIDGE_HOST_PORT:-${OPENCLAW_BRIDGE_PORT:-18790}}" +GATEWAY_BIND="${OPENCLAW_GATEWAY_BIND:-lan}" + +# Safe cwd for podman (openclaw is nologin; avoid inherited cwd from sudo) +cd "$EFFECTIVE_HOME" 2>/dev/null || cd /tmp 2>/dev/null || true + +RUN_SETUP=false +if [[ "${1:-}" == "setup" || "${1:-}" == "onboard" ]]; then + RUN_SETUP=true + shift +fi + +mkdir -p "$CONFIG_DIR" "$WORKSPACE_DIR" +# Subdirs the app may create at runtime (canvas, cron); create here so ownership is correct +mkdir -p "$CONFIG_DIR/canvas" "$CONFIG_DIR/cron" + +if [[ -f "$ENV_FILE" ]]; then + set -a + # shellcheck source=/dev/null + source "$ENV_FILE" 2>/dev/null || true + set +a +fi + +upsert_env_var() { + local file="$1" + local key="$2" + local value="$3" + local tmp + tmp="$(mktemp)" + 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 +} + +if [[ -z "${OPENCLAW_GATEWAY_TOKEN:-}" ]]; then + if command -v openssl &>/dev/null; then + export OPENCLAW_GATEWAY_TOKEN="$(openssl rand -hex 32)" + else + export OPENCLAW_GATEWAY_TOKEN="$(python3 - <<'PY' +import secrets +print(secrets.token_hex(32)) +PY +)" + fi + mkdir -p "$(dirname "$ENV_FILE")" + upsert_env_var "$ENV_FILE" "OPENCLAW_GATEWAY_TOKEN" "$OPENCLAW_GATEWAY_TOKEN" + echo "Generated OPENCLAW_GATEWAY_TOKEN and wrote it to $ENV_FILE." >&2 +fi + +PODMAN_USERNS="${OPENCLAW_PODMAN_USERNS:-keep-id}" +USERNS_ARGS=() +RUN_USER_ARGS=() +case "$PODMAN_USERNS" in + ""|auto) ;; + keep-id) USERNS_ARGS=(--userns=keep-id) ;; + host) USERNS_ARGS=(--userns=host) ;; + *) + echo "Unsupported OPENCLAW_PODMAN_USERNS=$PODMAN_USERNS (expected: keep-id, auto, host)." >&2 + exit 2 + ;; +esac + +RUN_UID="$(id -u)" +RUN_GID="$(id -g)" +if [[ "$PODMAN_USERNS" == "keep-id" ]]; then + RUN_USER_ARGS=(--user "${RUN_UID}:${RUN_GID}") + echo "Starting container as uid=${RUN_UID} gid=${RUN_GID} (must match owner of $CONFIG_DIR)" >&2 +else + echo "Starting container without --user (OPENCLAW_PODMAN_USERNS=$PODMAN_USERNS), mounts may require ownership fixes." >&2 +fi + +ENV_FILE_ARGS=() +[[ -f "$ENV_FILE" ]] && ENV_FILE_ARGS+=(--env-file "$ENV_FILE") + +if [[ "$RUN_SETUP" == true ]]; then + exec podman run --pull="$PODMAN_PULL" --rm -it \ + --init \ + "${USERNS_ARGS[@]}" "${RUN_USER_ARGS[@]}" \ + -e HOME=/home/node -e TERM=xterm-256color -e BROWSER=echo \ + -e OPENCLAW_GATEWAY_TOKEN="$OPENCLAW_GATEWAY_TOKEN" \ + -v "$CONFIG_DIR:/home/node/.openclaw:rw" \ + -v "$WORKSPACE_DIR:/home/node/.openclaw/workspace:rw" \ + "${ENV_FILE_ARGS[@]}" \ + "$OPENCLAW_IMAGE" \ + node dist/index.js onboard "$@" +fi + +podman run --pull="$PODMAN_PULL" -d --replace \ + --name "$CONTAINER_NAME" \ + --init \ + "${USERNS_ARGS[@]}" "${RUN_USER_ARGS[@]}" \ + -e HOME=/home/node -e TERM=xterm-256color \ + -e OPENCLAW_GATEWAY_TOKEN="$OPENCLAW_GATEWAY_TOKEN" \ + "${ENV_FILE_ARGS[@]}" \ + -v "$CONFIG_DIR:/home/node/.openclaw:rw" \ + -v "$WORKSPACE_DIR:/home/node/.openclaw/workspace:rw" \ + -p "${HOST_GATEWAY_PORT}:18789" \ + -p "${HOST_BRIDGE_PORT}:18790" \ + "$OPENCLAW_IMAGE" \ + node dist/index.js gateway --bind "$GATEWAY_BIND" --port 18789 + +echo "Container $CONTAINER_NAME started. Dashboard: http://127.0.0.1:${HOST_GATEWAY_PORT}/" +echo "Logs: podman logs -f $CONTAINER_NAME" +echo "For auto-start/restarts, use: ./setup-podman.sh --quadlet (Quadlet + systemd user service)." diff --git a/setup-podman.sh b/setup-podman.sh new file mode 100755 index 0000000000..fb8c3909a0 --- /dev/null +++ b/setup-podman.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env bash +# One-time host setup for rootless OpenClaw in Podman: creates the openclaw +# user, builds the image, loads it into that user's Podman store, and installs +# the launch script. Run from repo root with sudo capability. +# +# Usage: ./setup-podman.sh [--quadlet|--container] +# --quadlet Install systemd Quadlet so the container runs as a user service +# --container Only install user + image + launch script; 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 # onboarding wizard +# Or as the openclaw user: sudo -u openclaw /home/openclaw/run-openclaw-podman.sh +# If you used --quadlet, you can also: sudo systemctl --machine openclaw@ --user start openclaw.service +set -euo pipefail + +OPENCLAW_USER="${OPENCLAW_PODMAN_USER:-openclaw}" +REPO_PATH="${OPENCLAW_REPO_PATH:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" +RUN_SCRIPT_SRC="$REPO_PATH/scripts/run-openclaw-podman.sh" +QUADLET_TEMPLATE="$REPO_PATH/scripts/podman/openclaw.container.in" + +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 ]]; } + +run_root() { + if is_root; then + "$@" + else + sudo "$@" + fi +} + +run_as_user() { + local user="$1" + shift + if command -v sudo >/dev/null 2>&1; then + sudo -u "$user" "$@" + elif is_root && command -v runuser >/dev/null 2>&1; then + runuser -u "$user" -- "$@" + else + echo "Need sudo (or root+runuser) to run commands as $user." >&2 + exit 1 + fi +} + +# Quadlet: opt-in via --quadlet or OPENCLAW_PODMAN_QUADLET=1 +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 + +require_cmd podman +if ! is_root; then + require_cmd sudo +fi +if [[ ! -f "$REPO_PATH/Dockerfile" ]]; then + echo "Dockerfile not found at $REPO_PATH. Set OPENCLAW_REPO_PATH to the repo root." >&2 + exit 1 +fi +if [[ ! -f "$RUN_SCRIPT_SRC" ]]; then + echo "Launch script not found at $RUN_SCRIPT_SRC." >&2 + exit 1 +fi + +user_exists() { + local user="$1" + if command -v getent >/dev/null 2>&1; then + getent passwd "$user" >/dev/null 2>&1 && return 0 + fi + id -u "$user" >/dev/null 2>&1 +} + +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" +} + +resolve_nologin_shell() { + for cand in /usr/sbin/nologin /sbin/nologin /usr/bin/nologin /bin/false; do + if [[ -x "$cand" ]]; then + printf '%s' "$cand" + return 0 + fi + done + printf '%s' "/usr/sbin/nologin" +} + +# Create openclaw user (non-login, with home) if missing +if ! user_exists "$OPENCLAW_USER"; then + NOLOGIN_SHELL="$(resolve_nologin_shell)" + echo "Creating user $OPENCLAW_USER ($NOLOGIN_SHELL, with home)..." + if command -v useradd >/dev/null 2>&1; then + run_root useradd -m -s "$NOLOGIN_SHELL" "$OPENCLAW_USER" + elif command -v adduser >/dev/null 2>&1; then + # Debian/Ubuntu: adduser supports --disabled-password/--gecos. Busybox adduser differs. + run_root adduser --disabled-password --gecos "" --shell "$NOLOGIN_SHELL" "$OPENCLAW_USER" + else + echo "Neither useradd nor adduser found, cannot create user $OPENCLAW_USER." >&2 + exit 1 + fi +else + echo "User $OPENCLAW_USER already exists." +fi + +OPENCLAW_HOME="$(resolve_user_home "$OPENCLAW_USER")" +OPENCLAW_UID="$(id -u "$OPENCLAW_USER" 2>/dev/null || true)" +OPENCLAW_CONFIG="$OPENCLAW_HOME/.openclaw" +LAUNCH_SCRIPT_DST="$OPENCLAW_HOME/run-openclaw-podman.sh" + +# Prefer systemd user services (Quadlet) for production. Enable lingering early so rootless Podman can run +# without an interactive login. +if command -v loginctl &>/dev/null; then + run_root loginctl enable-linger "$OPENCLAW_USER" 2>/dev/null || true +fi +if [[ -n "${OPENCLAW_UID:-}" && -d /run/user && command -v systemctl &>/dev/null ]]; then + run_root systemctl start "user@${OPENCLAW_UID}.service" 2>/dev/null || true +fi + +# Rootless Podman needs subuid/subgid for the run user +if ! grep -q "^${OPENCLAW_USER}:" /etc/subuid 2>/dev/null; then + echo "Warning: $OPENCLAW_USER has no subuid range. Rootless Podman may fail." >&2 + echo " Add a line to /etc/subuid and /etc/subgid, e.g.: $OPENCLAW_USER:100000:65536" >&2 +fi + +echo "Creating $OPENCLAW_CONFIG and workspace..." +run_root mkdir -p "$OPENCLAW_CONFIG/workspace" +run_root chown -R "$OPENCLAW_USER:" "$OPENCLAW_CONFIG" + +if [[ ! -f "$OPENCLAW_CONFIG/.env" ]]; then + if command -v openssl >/dev/null 2>&1; then + TOKEN="$(openssl rand -hex 32)" + else + TOKEN="$(python3 - <<'PY' +import secrets +print(secrets.token_hex(32)) +PY +)" + fi + echo "OPENCLAW_GATEWAY_TOKEN=$TOKEN" | run_root tee "$OPENCLAW_CONFIG/.env" >/dev/null + run_root chown "$OPENCLAW_USER:" "$OPENCLAW_CONFIG/.env" + run_root chmod 600 "$OPENCLAW_CONFIG/.env" 2>/dev/null || true + echo "Created $OPENCLAW_CONFIG/.env with new token." +fi + +echo "Building image from $REPO_PATH..." +podman build -t openclaw:local -f "$REPO_PATH/Dockerfile" "$REPO_PATH" + +echo "Loading image into $OPENCLAW_USER's Podman store..." +TMP_IMAGE="$(mktemp -p /tmp openclaw-image.XXXXXX.tar)" +trap 'rm -f "$TMP_IMAGE"' EXIT +podman save openclaw:local -o "$TMP_IMAGE" +chmod 644 "$TMP_IMAGE" +(cd /tmp && run_as_user "$OPENCLAW_USER" env HOME="$OPENCLAW_HOME" podman load -i "$TMP_IMAGE") +rm -f "$TMP_IMAGE" +trap - EXIT + +echo "Copying launch script to $LAUNCH_SCRIPT_DST..." +run_root cp "$RUN_SCRIPT_SRC" "$LAUNCH_SCRIPT_DST" +run_root chown "$OPENCLAW_USER:" "$LAUNCH_SCRIPT_DST" +run_root chmod 755 "$LAUNCH_SCRIPT_DST" + +# Optionally install systemd quadlet for openclaw user (rootless Podman + systemd) +QUADLET_DIR="$OPENCLAW_HOME/.config/containers/systemd" +if [[ "$INSTALL_QUADLET" == true && -f "$QUADLET_TEMPLATE" ]]; then + echo "Installing systemd quadlet for $OPENCLAW_USER..." + run_root mkdir -p "$QUADLET_DIR" + sed "s|{{OPENCLAW_HOME}}|$OPENCLAW_HOME|g" "$QUADLET_TEMPLATE" | run_root tee "$QUADLET_DIR/openclaw.container" >/dev/null + run_root chown -R "$OPENCLAW_USER:" "$QUADLET_DIR" + if command -v systemctl &>/dev/null; then + run_root systemctl --machine "${OPENCLAW_USER}@" --user daemon-reload 2>/dev/null || true + run_root systemctl --machine "${OPENCLAW_USER}@" --user enable openclaw.service 2>/dev/null || true + run_root systemctl --machine "${OPENCLAW_USER}@" --user start openclaw.service 2>/dev/null || true + fi +fi + +echo "" +echo "Setup complete. Start the gateway:" +echo " $RUN_SCRIPT_SRC launch" +echo " $RUN_SCRIPT_SRC launch setup # onboarding wizard" +echo "Or as $OPENCLAW_USER (e.g. from cron):" +echo " sudo -u $OPENCLAW_USER $LAUNCH_SCRIPT_DST" +echo " sudo -u $OPENCLAW_USER $LAUNCH_SCRIPT_DST setup" +if [[ "$INSTALL_QUADLET" == true ]]; then + echo "Or use systemd (quadlet):" + echo " sudo systemctl --machine ${OPENCLAW_USER}@ --user start openclaw.service" + echo " sudo systemctl --machine ${OPENCLAW_USER}@ --user status openclaw.service" +else + echo "To install systemd quadlet later: $0 --quadlet" +fi