Rootless Podman deployment for claw-family AI agent platforms with Telegram integration. Supports four variants:
- OpenClaw — open-source AI agent platform
- PicoClaw — lightweight variant
- Gclaw — fork of PicoClaw
- Tradeclaw — fork of PicoClaw for direct Base chain trading (Go binary)
1. Configure your environment
cp .env.example .env
vi .env # Set your host, variant, IPs, ports, and resource limitsAll scripts source .env automatically. See .env.example for all available options.
Recommended env vars to set:
CLAW_VARIANT=openclaw
CLAW_HOST=your.host.or.ip
CLAW_SSH_USER=sshuser
CLAW_USER=openclaw
CLAW_HOME=/data/openclaw
2. Run deployment scripts (as root on target host)
bash scripts/01-create-user.sh # Create dedicated nologin system user
bash scripts/02-setup-podman.sh # Configure rootless Podman
bash scripts/03-clone-and-build.sh # Clone source and build container image
bash scripts/04-configure.sh # Generate hardened config + gateway token
bash scripts/05-deploy-quadlet.sh # Deploy systemd quadlet (auto-starts on boot)Set CLAW_VARIANT in .env before running to deploy a different variant.
3. Post-deploy (interactive)
# Add Telegram bot — get token from @BotFather
podman exec -it openclaw openclaw channels add --channel telegram --token <TOKEN>
# Run configure wizard — set up Anthropic OAuth
podman exec -it openclaw openclaw configure
# Restart after config changes
systemctl --user restart openclaw.service
# Pair your Telegram account — send any message to bot, then approve
podman exec openclaw openclaw devices approve --latestAll
podmanandsystemctl --usercommands run as the service user. See Operations for the full invocation pattern.
Commands run as the service user (configured in .env):
# Service lifecycle (container name matches variant)
systemctl --user status openclaw.service
systemctl --user restart openclaw.service
# Container logs
podman logs --tail 50 openclaw
# Config changes
podman exec openclaw openclaw config set <key> <value>
# Manual backup
bash multi-instance/backup.shSSH access pattern:
ssh <host> 'sudo -u <user> XDG_RUNTIME_DIR=/run/user/$(id -u <user>) <command>'
Codex OAuth requires a recent build that includes the OpenAI/Codex auth flow. If you see "No provider plugins found" in the container, rebuild the image from the upstream repo and restart the service.
Rebuild (on host, as the service user):
cd /data/openclaw/openclaw-src
git pull --ff-only
podman build -t openclaw:local -f Dockerfile .Restart:
sudo -u openclaw XDG_RUNTIME_DIR=/run/user/$(id -u openclaw) systemctl --user restart openclaw.serviceRun Codex OAuth (interactive TTY required):
ssh -tt "${CLAW_SSH_USER:-user}@${CLAW_HOST:-host}" \
"SVC_USER='${CLAW_USER:-openclaw}' && SVC_UID=\$(id -u \"\$SVC_USER\") && \
cd /tmp && sudo -u \"\$SVC_USER\" XDG_RUNTIME_DIR=\"/run/user/\$SVC_UID\" \
podman exec -it openclaw node dist/index.js onboard --auth-choice openai-codex"Then set the default model and restart the gateway:
claw default config set agents.defaults.model.primary openai-codex/gpt-5.3-codex
claw default gateway restartSymptom:
gateway token mismatchwhen running CLI commands.
Fix (on host):
SVC_USER="${CLAW_USER:-openclaw}"
SVC_UID="$(id -u "$SVC_USER")"
CLAW_HOME="${CLAW_HOME:-/data/openclaw}"
VARIANT="${CLAW_VARIANT:-openclaw}"
TOKEN="$(sudo -u "$SVC_USER" sed -n "s/^${VARIANT^^}_GATEWAY_TOKEN=//p" "$CLAW_HOME/.${VARIANT}/.env")"
sudo -u "$SVC_USER" XDG_RUNTIME_DIR="/run/user/$SVC_UID" podman exec -i "$VARIANT" node dist/index.js config set gateway.auth.token "$TOKEN"
sudo -u "$SVC_USER" XDG_RUNTIME_DIR="/run/user/$SVC_UID" podman exec -i "$VARIANT" node dist/index.js config set gateway.remote.token "$TOKEN"
sudo -u "$SVC_USER" XDG_RUNTIME_DIR="/run/user/$SVC_UID" systemctl --user restart "${VARIANT}.service"Symptom:
systemctl --user unavailableon your local machine.
Fix:
ssh "${CLAW_SSH_USER:-user}@${CLAW_HOST:-host}" \
"cd /tmp && SVC_USER='${CLAW_USER:-openclaw}' && SVC_UID=\$(id -u \"\$SVC_USER\") && \
sudo -u \"\$SVC_USER\" XDG_RUNTIME_DIR=\"/run/user/\$SVC_UID\" systemctl --user restart ${CLAW_VARIANT:-openclaw}.service"The gateway UI is loopback-only. Access via SSH tunnel:
ssh -L 18789:127.0.0.1:18789 your-server
# Open http://localhost:18789Gateway token is in <CLAW_HOME>/.<variant>/.env.
Automated daily at 3am via systemd timer. 14-day retention, gzip compressed.
# Check timer
systemctl --user list-timers
# List backups
ls -lh backups/Run multiple isolated instances (of any variant) sharing a common skills library. See multi-instance/README.md for full documentation.
# Quick start
bash claw-instance.sh create research # New openclaw instance
bash claw-instance.sh create pico-dev --variant picoclaw # New picoclaw instance
bash claw-instance.sh list # Show all instances
bash claw-instance.sh start research # Start it
bash claw-instance.sh destroy research # Remove itclaw is a shell wrapper that provides a unified interface for managing instances — both locally on the host and remotely via SSH. It auto-detects whether you are on the server or a remote machine and routes commands accordingly.
From the repo directory (works on any POSIX system):
ln -sf $(pwd)/claw ~/.local/bin/clawEnsure ~/.local/bin is in your PATH. The script resolves symlinks to find its .env, so the .env file must be alongside the real script.
claw <instance> <command...> Run a command on an instance
claw <instance> --shell Drop into a shell inside the container
claw <instance> --logs [N] Tail container logs (default 50)
claw list List all instances
claw help Show this help
claw default devices list
claw default devices approve --latest
claw default config set gateway.bind loopback
claw research configure
claw default --shell
claw default --logs 100- Remote: wraps commands in
ssh -t <host> 'sudo -u <user> ...'using values from.env - Local: wraps commands in
sudo -u <user> ...directly (detected via hostname match) - Interactive commands (
configure,--shell, etc.) automatically allocate a TTY - Instance names are globally unique across variants — no need to specify which variant
<host>
├── Service user (nologin, rootless Podman with subuid/subgid)
├── Container (quadlet-managed, auto-restart)
│ ├── Gateway: 127.0.0.1:<port> (loopback only)
│ ├── Bridge: 127.0.0.1:<port> (loopback only)
│ └── Resources: configurable RAM + CPU limits
├── State: .<variant>/ # e.g. .openclaw/, .picoclaw/
│ ├── <variant>.json # Config
│ ├── .env # Gateway token
│ ├── agents/ # Auth profiles, sessions
│ ├── credentials/ # Telegram pairing
│ └── sandboxes/ # Agent skills + identity
├── Workspace: workspace/
└── Backups: backups/ # Daily, 14-day retention
Default ports per variant:
| Variant | Gateway | Bridge | Runtime |
|---|---|---|---|
| openclaw | 18789 | 18790 | Node.js |
| picoclaw | 28789 | 28790 | Node.js |
| gclaw | 38789 | 38790 | Node.js |
| tradeclaw | 48789 | 48790 | Go |
Tradeclaw is a Go binary, not a Node.js app. Key differences from Node.js variants:
- Quadlet Exec line: Use
Exec=gateway(notExec=node dist/index.js gateway --bind lan --port <port>). The ENTRYPOINT istradeclaw, so this runstradeclaw gateway. - Gateway port: Set in config JSON (
gateway.port) or viaTRADECLAW_GATEWAY_PORTenv var — no--portCLI flag. - OAuth login: Requires
--userns=keep-idso the container can write auth tokens to the mounted volume:
sudo -u tradeclaw -H bash -c "cd /data/tradeclaw && XDG_RUNTIME_DIR=/run/user/\$(id -u tradeclaw) \
podman run --rm -it --userns=keep-id \
-v /data/tradeclaw/.tradeclaw:/home/node/.tradeclaw \
-e HOME=/home/node --user \$(id -u tradeclaw):\$(id -g tradeclaw) \
localhost/tradeclaw:local auth login --provider openai"Auth tokens are saved to the mounted volume (/data/tradeclaw/.tradeclaw/) and survive image rebuilds and container restarts.
Podman Quadlet requires version 4.4+. On older versions (e.g. Debian bookworm ships 4.3.1), create a systemd user service manually instead of running 05-deploy-quadlet.sh:
# Create service directory
sudo -u <user> mkdir -p <home>/.config/systemd/user
# Write service file (example for tradeclaw)
cat > <home>/.config/systemd/user/<variant>.service << 'EOF'
[Unit]
Description=<variant> gateway (rootless Podman)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
Environment=XDG_RUNTIME_DIR=/run/user/<uid>
EnvironmentFile=<home>/.<variant>/.env
ExecStartPre=-/usr/bin/podman rm -f <variant>
ExecStart=/usr/bin/podman run --rm --name <variant> \
--userns=keep-id \
-p 127.0.0.1:<gateway_port>:<gateway_port> \
-p 127.0.0.1:<bridge_port>:<bridge_port> \
-v <home>/.<variant>:/home/node/.<variant> \
-v <home>/workspace:/home/node/.<variant>/workspace \
--env-file <home>/.<variant>/.env \
-e HOME=/home/node \
-e TERM=xterm-256color \
--dns 100.100.100.100 \
--dns 1.1.1.1 \
--memory=8g --cpus=4 \
--user <uid>:<gid> \
localhost/<variant>:local gateway
ExecStop=/usr/bin/podman stop -t 30 <variant>
Restart=on-failure
TimeoutStartSec=300
[Install]
WantedBy=default.target
EOF
# Enable and start
systemctl --machine <user>@ --user daemon-reload
systemctl --machine <user>@ --user enable --now <variant>.serviceFor private repos (e.g. tradeclaw), 03-clone-and-build.sh needs git access. Set up an SSH deploy key:
# Generate key for the service user
sudo -u <user> -H ssh-keygen -t ed25519 -f <home>/.ssh/id_ed25519 -N "" -C "<user>@<host>"
# Add to GitHub as a deploy key
gh repo deploy-key add <home>/.ssh/id_ed25519.pub --repo <owner>/<repo> --title "<user>@<host>"
# Add GitHub to known_hosts
sudo -u <user> -H bash -c "ssh-keyscan github.com >> <home>/.ssh/known_hosts"
# Rewrite HTTPS to SSH for the service user
sudo -u <user> -H git config --global url."git@github.com:".insteadOf "https://github.com/"If podman build fails with short-name did not resolve to an alias, add Docker Hub as a search registry:
cat >> /etc/containers/registries.conf << EOF
unqualified-search-registries = ["docker.io"]
EOF| Control | Detail |
|---|---|
| Dedicated nologin user | No shell access, isolated home directory |
| Rootless Podman | User namespace isolation via subuid/subgid |
| Loopback-only ports | Host binds to 127.0.0.1, access via SSH tunnel only |
| Token auth | Gateway requires token for all connections |
| Exec denied | tools.exec.security: "deny" |
| Dangerous tools denied | gateway, sessions_spawn, sessions_send blocked |
| Sandbox off | Container itself is the isolation boundary |
| Filesystem restricted | tools.fs.workspaceOnly: true |
| mDNS off | discovery.mdns.mode: "off" |
| Log redaction | logging.redactSensitive: "tools" |
subuid/subgid not configured
Symptom: podman build fails with potentially insufficient UIDs or GIDs available in user namespace.
Fix: Add non-overlapping range to /etc/subuid and /etc/subgid, then:
podman system reset --force
podman system migrateThe reset is critical — without it the UID mapping won't update.
Container can't read mounted config (permission denied)
Symptom: Permission denied on /home/node/.<variant>/ inside container.
Cause: --userns keep-id with Dockerfile's USER node (UID 1000) doesn't match file ownership.
Fix: Add --user <uid>:<gid> to quadlet PodmanArgs to match the service user's UID/GID.
Gateway unreachable (connection reset)
Symptom: curl http://127.0.0.1:<port>/ returns connection reset.
Cause: Podman pasta networking forwards via non-loopback 169.254.1.2. Gateway bound to loopback rejects it.
Fix: Use --bind lan in the Exec command (container listens on all interfaces). Enforce loopback at the host level with PublishPort=127.0.0.1:<port>:<port>.
CLI error: "plaintext ws:// to non-loopback"
Cause: gateway.bind in config controls both the listener AND the CLI connection URL. With "lan", CLI resolves the LAN IP which fails the security check.
Fix: Set gateway.bind: "loopback" in config. The quadlet Exec flag (--bind lan) independently controls the actual listener.
Cron/device pairing errors
Internal tools (cron, etc.) are treated as separate "devices" needing one-time pairing:
podman exec <container> <variant-cli> devices approve --latestAlso ensure cron is not in the tools.deny array in config.
Docker EACCES in agent sandbox
Cause: sandbox.mode: "all" tries to spawn Docker containers inside Podman. Docker isn't available.
Fix: <variant-cli> config set agents.defaults.sandbox.mode off — the Podman container is the sandbox.