Running OpenClaw in a Rootless Container (Because Root is for Amateurs)
This guide walks through setting up OpenClaw in a rootless Podman container on Arch Linux — no root, no Docker daemon, no unnecessary risk.
What is OpenClaw?
OpenClaw is an open-source personal AI assistant that you run on your own hardware. It connects to the messaging platforms you already use — Signal, Telegram, Discord, Slack, WhatsApp, iMessage — and gives you a single always-on assistant that can read files, run commands, browse the web, manage your calendar, and basically act as a second brain. Think of it as self-hosted ChatGPT, but with actual access to your stuff.
It supports multiple AI providers (Anthropic, OpenAI, local models via Ollama) and most people run it directly on their machine — Mac Minis, laptops, Raspberry Pis — installed as a user-level service. That works great for most setups.
But here’s the thing: if it’s running on bare metal as your user, the agent has access to everything on your computer. Your SSH keys, your credentials, your home directory, your browsing history — all of it. And remember, every command the agent runs, every file it reads, that context is being sent to whichever AI provider is powering it — Anthropic, OpenAI, or whoever else. That might not be something you want to share. A container gives you control over exactly what the agent can see and touch.
A rootless Podman container keeps the agent isolated without the complexity of setting up and maintaining a full VM. It’s not quite as strong a boundary as a VM — containers share the host kernel — but it’s far less to manage. The agent runs as a non-root node user inside a container that itself has no root privileges on the host. If something goes wrong — a bad prompt, unintended code execution, whatever — it’s stuck in an unprivileged container, and even a container escape lands as an unprivileged user. Your host and the rest of your network stay untouched.
The principle is simple: give it what it needs, wall off everything else.
The Ephemeral Box
Every time you rebuild the image, you get a fresh container. New OS, new packages, clean slate. Nothing persists inside the container itself — no leftover state, no “it worked a minute ago.” If something breaks, you rebuild and restart — no reinstalling your OS, no trying to fix a broken box. Just a clean slate in under a minute.
But OpenClaw needs memory. It builds up context over time, using workspace files, conversation history, credentials, and configuration. If all of that lived inside the container, every rebuild would be a lobotomy. We’ll use volume mounts to keep OpenClaw’s memory persistent across rebuilds.
Host filesystem (persists) Container (ephemeral)
───────────────────────── ─────────────────────
~/.openclaw/ → /home/node/.openclaw/
├── config.json (workspace, memory, creds)
├── workspace/
│ ├── SOUL.md
│ ├── MEMORY.md
│ └── memory/
└── credentials/
~/container_local/ → /home/node/.local/
└── share/ (signal-cli, claude cli)
Tear down the container, rebuild it, replace it entirely — OpenClaw’s knowledge and configuration persists.
Why Podman?
Docker’s rootless mode exists, but it was added after the fact — the Docker daemon was designed to run as root, and it shows. Rootless Docker still has limitations with AppArmor, cgroups, and networking that can bite you in production.
Podman was built rootless from the start. No daemon, no root requirement, each container can run as a user. Docker requires a background daemon (dockerd) running as root to manage containers — if it crashes, everything goes down, and dead containers can linger as zombies. Podman doesn’t have that. Each container is just a process. If it dies, it’s gone. This isn’t a knock on Docker — it’s a great tool and the right choice for a lot of workloads. But for running an AI agent with access to your personal data, Podman’s rootless-first design is a better fit.
Install Podman and Create Rootless User
Install Podman and Dependencies
1 | sudo pacman -S podman podman-compose fuse-overlayfs slirp4netns |
Packages:
- podman - Container engine
- podman-compose - Docker Compose compatibility
- fuse-overlayfs - Efficient storage driver for rootless containers
- slirp4netns - Rootless networking
Note: Podman defaults to pasta for rootless networking, but I’ve had better luck with slirp4netns.
Create a Dedicated Podman User
1 | sudo useradd -m -s /bin/bash podman |
No password on purpose — this is a service account, not a login account. Switch to it with sudo -iu podman when you need it.
Configure Subuid/Subgid Mappings
Rootless Podman needs subordinate UID/GID ranges for user namespace mapping:
echo "podman:100000:65536" | sudo tee -a /etc/subuid
echo "podman:100000:65536" | sudo tee -a /etc/subgid
Verify:
cat /etc/subuid
cat /etc/subgid
Enable Lingering
Allows containers to run without an active login session:
1 | sudo loginctl enable-linger podman |
Switch to the Podman User
1 | sudo -iu podman |
Verify Rootless Podman
1 2 3 | podman --version podman info --format '{{.Host.Security.Rootless}}' # Should return "true" podman run --rm hello-world |
Set Up OpenClaw
Clone the Repository
Switch to the podman user first if you haven’t already:
sudo -iu podman
cd ~
git clone https://github.com/openclaw/openclaw.git
cd openclaw
git fetch --tags
git checkout $(git tag --sort=-v:refname | head -1) # Latest stable release
Create Directories
1 2 | mkdir -p ~/.openclaw/workspace ~/.openclaw/credentials mkdir -p ~/container_local/share |
~/.openclaw/- Main config directory (mounted to container)~/container_local/- Container’s .local directory (for Claude CLI config, signal-cli data, etc.)
Create Environment File
Create ~/openclaw/.env:
1 2 3 4 5 6 | OPENCLAW_CONFIG_DIR=/home/podman/.openclaw OPENCLAW_GATEWAY_PORT=18789 OPENCLAW_BRIDGE_PORT=18790 OPENCLAW_GATEWAY_BIND=lan OPENCLAW_GATEWAY_TOKEN=<generate-a-secure-token> OPENCLAW_IMAGE=openclaw:local |
Generate a secure token:
1 | openssl rand -hex 32 |
Create the Management Script
Create ~/clawd.sh:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | #!/bin/bash COMPOSE_FILE="$HOME/openclaw/docker-compose.podman.yml" case "$1" in start) podman-compose -f "$COMPOSE_FILE" up -d openclaw-gateway ;; stop) podman-compose -f "$COMPOSE_FILE" down ;; restart) podman-compose -f "$COMPOSE_FILE" down podman-compose -f "$COMPOSE_FILE" up -d openclaw-gateway ;; logs) podman logs -f openclaw_openclaw-gateway_1 ;; status) podman ps -a --filter name=openclaw ;; build) podman-compose -f "$COMPOSE_FILE" build ;; cli) shift podman-compose -f "$COMPOSE_FILE" run --rm openclaw-cli "$@" ;; shell) podman exec -it openclaw_openclaw-gateway_1 bash ;; tui) podman exec -it openclaw_openclaw-gateway_1 node dist/index.js tui ;; *) echo "Usage: $0 {start|stop|restart|logs|status|build|cli|shell|tui}" exit 1 ;; esac |
Make it executable:
1 | chmod +x ~/clawd.sh |
Why a Custom Dockerfile?
OpenClaw’s default image is built on node:22-bookworm — a bare Node.js container. That’s fine for running the gateway itself, but the agent inside needs to do things: parse PDFs, process video, extract text from images, automate browsers, communicate over Signal. A stock Node.js container can’t do any of that.
Rather than exec-ing into the container as root every time you need a package (which defeats the point of rootless), we build a custom image with everything baked in. The tradeoff is a larger image (~1.5GB vs ~400MB), but you get a self-contained environment where OpenClaw has the tools it needs without runtime privilege escalation.
The full Dockerfile and docker-compose file are at the bottom of this post.
Build the Image
1 | ~/clawd.sh build
|
Start the Gateway
1 | ~/clawd.sh start
|
Verify:
1 2 3 | ~/clawd.sh status ss -tlnp | grep 18789 ~/clawd.sh logs |
Managing the Container
| Command | Description |
|---|---|
~/clawd.sh start |
Start the gateway |
~/clawd.sh stop |
Stop the gateway |
~/clawd.sh restart |
Restart the gateway |
~/clawd.sh logs |
Follow container logs |
~/clawd.sh status |
Show container status |
~/clawd.sh build |
Rebuild the image |
~/clawd.sh cli <args> |
Run OpenClaw CLI commands |
~/clawd.sh shell |
Get a bash shell in the container |
~/clawd.sh tui |
Launch the Terminal UI |
Using OpenClaw
Web UI
Open in browser: http://localhost:18789
Terminal UI (TUI)
1 | ~/clawd.sh tui
|
Claude CLI
Get a shell and run Claude:
1 2 | ~/clawd.sh shell
claude
|
First run requires authentication setup.
OpenClaw CLI
1 2 3 | ~/clawd.sh cli --help ~/clawd.sh cli message send "hello" ~/clawd.sh cli doctor |
Updating
1 2 3 4 5 | cd ~/openclaw git fetch --tags git checkout $(git tag --sort=-v:refname | head -1) ~/clawd.sh build ~/clawd.sh restart |
See available releases:
1 | git tag --sort=-v:refname | head -10 |
File Locations
| Path | Description |
|---|---|
~/.openclaw/ |
Main config directory |
~/.openclaw/workspace/ |
Agent workspace (SOUL.md, IDENTITY.md, etc.) |
~/.openclaw/credentials/ |
Stored credentials |
~/.openclaw/config.json |
Gateway configuration |
~/container_local/ |
Container’s .local directory |
~/container_local/share/signal-cli/ |
Signal account data |
~/container_local/share/claude/ |
Claude CLI config |
~/openclaw/ |
Source repository |
~/openclaw/.env |
Environment variables |
~/openclaw/docker-compose.podman.yml |
Compose file |
~/openclaw/Dockerfile.podman |
Custom Dockerfile |
~/clawd.sh |
Management script |
Troubleshooting
Permission denied on volumes
Fix ownership on host:
1 | sudo chown -R podman:podman ~/.openclaw ~/container_local |
Container exits immediately
Check logs:
1 | ~/clawd.sh logs
|
“No such device” error for /dev/net/tun
Expected in nested containers/VMs. Host networking is used instead.
npm/Claude CLI permission errors
The Dockerfile.podman fixes /home/node ownership. If issues persist:
1 | podman exec -u root openclaw_openclaw-gateway_1 chown -R node:node /home/node |
Configuration Files
docker-compose.podman.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | # Rootless Podman compose file for OpenClaw # Uses host networking (TUN device not available for slirp4netns/pasta) services: openclaw-gateway: build: context: . dockerfile: Dockerfile.podman network: host image: ${OPENCLAW_IMAGE:-openclaw:local} network_mode: host environment: HOME: /home/node TERM: xterm-256color OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN} CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY} CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY} CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE} volumes: - ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw:U - ${HOME}/container_local:/home/node/.local:U init: true restart: unless-stopped command: [ "node", "dist/index.js", "gateway", "--bind", "${OPENCLAW_GATEWAY_BIND:-lan}", "--port", "${OPENCLAW_GATEWAY_PORT:-18789}", "--allow-unconfigured", ] openclaw-cli: image: ${OPENCLAW_IMAGE:-openclaw:local} network_mode: host environment: HOME: /home/node TERM: xterm-256color OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN} BROWSER: echo CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY} CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY} CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE} volumes: - ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw:U - ${HOME}/container_local:/home/node/.local:U stdin_open: true tty: true init: true entrypoint: ["node", "dist/index.js"] |
Dockerfile.podman
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | FROM node:22-bookworm # Install Bun (required for build scripts) RUN curl -fsSL https://bun.sh/install | bash ENV PATH="/root/.bun/bin:${PATH}" RUN corepack enable WORKDIR /app # Install Chromium, Java (for signal-cli), and system utilities RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ chromium \ xvfb \ default-jre-headless \ tesseract-ocr \ poppler-utils \ jq \ ffmpeg \ sqlite3 \ pandoc \ tree \ zip \ lynx \ htop \ && apt-get clean && \ rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* # Install Claude CLI globally RUN npm install -g @anthropic-ai/claude-code COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ COPY ui/package.json ./ui/package.json COPY patches ./patches COPY scripts ./scripts RUN pnpm install --frozen-lockfile COPY . . RUN OPENCLAW_A2UI_SKIP_MISSING=1 pnpm build ENV OPENCLAW_PREFER_PNPM=1 RUN pnpm ui:build ENV NODE_ENV=production # Fix home directory ownership for node user RUN chown -R node:node /app /home/node USER node CMD ["node", "dist/index.js"] |
Packages included: - chromium — Browser automation for web scraping and interaction - xvfb — Virtual framebuffer for running Chromium in headed mode without a display - default-jre-headless — Java runtime for signal-cli - tesseract-ocr — OCR for image-based PDFs - poppler-utils — PDF text extraction (pdftotext) - jq — JSON parsing in shell - ffmpeg — Video/audio processing - sqlite3 — Database queries - pandoc — Document format conversion - tree — Directory visualization - zip — Archive creation - lynx — Text-based web browsing - htop — Process monitoring - Claude CLI — Anthropic’s CLI for Claude
Security Notes
- Container runs as non-root user
node(UID 1000) OPENCLAW_GATEWAY_TOKENprotects the gateway API- Host networking exposes ports directly on host interface
- Volumes use
:Usuffix for automatic UID mapping
Wrapping Up
At this point you’ve got OpenClaw running in an isolated, rootless container with its data safely persisted on the host. It can do its thing — run commands, parse files, browse the web — without having the keys to the rest of your machine.
From here, you can configure messaging channels (Signal, Telegram, Discord, etc.) through the OpenClaw CLI, set up skills and automations, and start customizing the agent’s workspace with your own memory files and tools. Check out the OpenClaw docs for the full rundown.
The beauty of this setup is that updates are painless: pull the latest release, rebuild, restart. Your agent’s brain survives every change. And if something ever goes sideways, you’re one build away from a clean slate.