mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
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 <steipete@gmail.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -142,6 +142,9 @@ The **installer script** is the recommended way to install OpenClaw. It handles
|
||||
<Card title="Docker" href="/install/docker" icon="container">
|
||||
Containerized or headless deployments.
|
||||
</Card>
|
||||
<Card title="Podman" href="/install/podman" icon="container">
|
||||
Rootless container: run `setup-podman.sh` once, then the launch script.
|
||||
</Card>
|
||||
<Card title="Nix" href="/install/nix" icon="snowflake">
|
||||
Declarative install via Nix.
|
||||
</Card>
|
||||
|
||||
105
docs/install/podman.md
Normal file
105
docs/install/podman.md
Normal file
@@ -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.
|
||||
24
openclaw.podman.env
Normal file
24
openclaw.podman.env
Normal file
@@ -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=
|
||||
26
scripts/podman/openclaw.container.in
Normal file
26
scripts/podman/openclaw.container.in
Normal file
@@ -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
|
||||
189
scripts/run-openclaw-podman.sh
Executable file
189
scripts/run-openclaw-podman.sh
Executable file
@@ -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)."
|
||||
215
setup-podman.sh
Executable file
215
setup-podman.sh
Executable file
@@ -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
|
||||
Reference in New Issue
Block a user