Automated deployment of OpenClaw with rootless Docker, nftables firewall, and proper UID/GID mapping.
Designed for cloud servers, dedicated servers, and VPS instances. Also suitable for local installations.
This deployment automates the complete setup of OpenClaw as a non-root service using rootless Docker.
OpenClaw is an AI assistant that executes code and commands on your server. Running such a system as root is extremely risky—a compromised container would have complete control over your host. This deployment provides a secure, production-ready setup that:
- Isolates OpenClaw from the host system
- Minimizes attack surface through proper firewall configuration
- Uses least-privilege principles throughout
- Automates security best practices
| Risk | Without This Deployment | How This Repo Helps |
|---|---|---|
| Root privilege escalation | OpenClaw runs as root = full system access | Runs as unprivileged user with UID namespace isolation |
| Firewall misconfiguration | Manual setup often breaks Docker networking | Pre-configured nftables with Docker-aware rules |
| Container escape impact | Root container = host takeover | Non-root container limits damage scope |
| Unnecessary exposed ports | Services exposed to internet | Deny-all firewall with only essential ports open |
| Docker daemon breaks on firewall reload | Manual flush ruleset wipes NAT rules |
Uses table-specific flush + systemd override |
| Feature | Security Benefit |
|---|---|
| Rootless Docker | OpenClaw has no root privileges; container escape doesn't grant root access |
| UID namespace remapping | Container UIDs map to different host UIDs, adding isolation layer |
| nftables (deny-all) | Only SSH (rate-limited), HTTP/HTTPS, and gateway port allowed |
| Docker-aware firewall | Forward chain explicitly allows bridge traffic only |
| No exposed dashboard | Gateway binds to localhost; requires SSH tunnel for access |
| Docker survives firewall reload | Systemd override prevents service disruption |
- Rootless Docker: OpenClaw runs without root privileges
- nftables firewall: Pre-configured with Docker-aware rules
- UID mapping: Automatic configuration for container file access
- Systemd integration: Docker survives firewall reloads
- SSH-only dashboard access: Gateway requires tunnel for access
| Environment | Recommended | Notes |
|---|---|---|
| Cloud servers (AWS, GCP, Azure, DigitalOcean, etc.) | ✅ Yes | Ideal for production |
| Dedicated servers (Hetzner, OVH, etc.) | ✅ Yes | Full isolation |
| VPS instances | ✅ Yes | Resource-efficient |
| Local/VM (Linux) | ✅ Yes | Great for development |
| WSL2 | May need additional config | |
| macOS | ❌ Not supported | nftables not available |
# Clone and run installer
git clone https://github.com/krazyuniks/rootless-docker-openclaw.git /tmp/rootless-docker-openclaw
cd /tmp/rootless-docker-openclaw
sudo ./install.shThe installer will:
- Create the
openclawuser - Configure nftables firewall (with Docker-forward rules)
- Install rootless Docker
- Clone OpenClaw source and run setup
- Start the gateway
After installation, the complete structure is:
/home/openclaw/
├── openclaw/ # Cloned from upstream during install
│ ├── docker-compose.yml # Docker compose config
│ ├── docker-setup.sh # Official setup script
│ ├── Dockerfile # Container build
│ ├── dist/ # Built application
│ ├── apps/ # OpenClaw apps
│ ├── agents/ # AI agents
│ └── ...
│
├── rootless-docker-openclaw/ # This deployment repo
│ ├── install.sh # Main installer (chains all scripts)
│ ├── README.md # This file
│ ├── .gitignore
│ └── configs/
│ ├── nftables.conf # Firewall rules (Docker-aware)
│ ├── docker-daemon.json # Docker DNS config
│ └── systemd-docker-override.conf # Docker restarts after nftables
│ └── scripts/
│ ├── 01-user-setup.sh # Create openclaw user
│ ├── 02-firewall.sh # Install nftables
│ ├── 03-docker-rootless.sh # Install rootless Docker
│ ├── 04-openclaw.sh # Clone & setup OpenClaw
│ └── 05-start.sh # Start gateway
│
├── .openclaw/ # Runtime config (created by OpenClaw)
│ ├── openclaw.json # Gateway configuration
│ ├── workspace/ # Agent workspace
│ ├── agents/ # AI agent configs
│ ├── credentials/ # API keys
│ ├── canvas/ # Agent canvas
│ ├── cron/ # Scheduled tasks
│ ├── telegram/ # Telegram bot config
│ └── devices/ # Paired devices
│
├── .config/
│ └── docker/
│ └── daemon.json # Docker daemon config (DNS)
│
└── bin/ # Rootless Docker binaries
├── docker
├── dockerd
├── containerd
├── runc
└── ...
| Feature | Description |
|---|---|
| Rootless Docker | OpenClaw runs as non-root user with UID namespace remapping |
| nftables firewall | Properly configured to work with Docker (avoids flush ruleset) |
| Systemd integration | Docker restarts after nftables reloads |
| UID mapping | Files owned correctly for container access |
| Automated install | Single script handles entire setup |
# Create SSH tunnel from your local machine
ssh -L 18789:127.0.0.1:18789 openclaw@<server>
# Open in browser
http://127.0.0.1:18789/?token=<your-token>If your server is only accessible through a jump host:
# Add to ~/.ssh/config on your local machine
Host bastion
HostName bastion.example.com
User your-user
Host openclaw-server
HostName 192.168.1.100 # Private IP of OpenClaw server
User openclaw
ProxyJump bastion
LocalForward 18789 127.0.0.1:18789
# Connect with single command
ssh openclaw-server
# Dashboard available at
http://127.0.0.1:18789/?token=<your-token>ssh -L 18789:127.0.0.1:18789 -l openclaw <server-hostname>When you first access the dashboard, you'll see a "pairing required" error. This is a security feature - each browser/device must be approved before it can connect.
# List pending pairing requests
sudo -u openclaw docker exec openclaw-gateway node dist/index.js devices list
# You'll see output like:
# Pending (1)
# ┌──────────────────────────────────────┬─────────────┬──────────┬────────────┐
# │ Request │ Device │ Role │ IP │
# ├──────────────────────────────────────┼─────────────┼──────────┼────────────┤
# │ 6021dc52-04d9-42e6-826e-f9b620a19a3a │ 15c051a... │ operator │ 172.18.0.1 │
# └──────────────────────────────────────┴─────────────┴──────────┴────────────┘
# Approve using the Request ID (first column)
sudo -u openclaw docker exec openclaw-gateway node dist/index.js devices approve 6021dc52-04d9-42e6-826e-f9b620a19a3aAfter approval, refresh your browser - you should now be connected.
sudo -u openclaw docker exec openclaw-gateway node dist/index.js devices list# Get your auth token
sudo -u openclaw cat /home/openclaw/.openclaw/openclaw.json | jq -r '.gateway.auth.token'
# Start gateway
sudo -u openclaw docker run -d --rm -p 18789:18789 -v ~/.openclaw:/home/node/.openclaw --name openclaw-gateway openclaw:local node dist/index.js gateway --bind lan
# Stop gateway
sudo -u openclaw docker rm -f openclaw-gateway
# CLI commands (while gateway running)
sudo -u openclaw docker exec openclaw-gateway node dist/index.js devices list
sudo -u openclaw docker exec openclaw-gateway node dist/index.js devices approve <REQUEST_ID>
sudo -u openclaw docker exec openclaw-gateway node dist/index.js pairing approve telegram <CODE>sudo -u openclaw docker logs -f openclaw-gatewaycd /home/openclaw/rootless-docker-openclaw
sudo ./scripts/05-start.shcd /home/openclaw/openclaw
sudo -u openclaw git pull
./docker-setup.sh
sudo ../rootless-docker-openclaw/scripts/05-start.shThe server uses nftables alongside Docker's iptables-nft rules.
| Component | Table | Purpose |
|---|---|---|
| Base firewall | inet filter |
SSH, HTTP/HTTPS, input filtering |
| Docker | ip filter, ip nat |
Container networking, MASQUERADE |
The inet filter forward chain must allow Docker traffic:
chain forward {
type filter hook forward priority 0; policy drop;
# Allow Docker container traffic
iifname "docker*" accept
iifname "br-*" accept
oifname "docker*" accept
oifname "br-*" accept
# Allow established/related for return traffic
ct state established,related accept
}
Using flush ruleset in nftables.conf wipes Docker's NAT rules, breaking container networking. Use table-specific flush instead:
# Only flush our table, not Docker's
flush table inet filter
If containers lose internet connectivity after nftables changes:
# Test container connectivity
sudo -u openclaw docker run --rm alpine ping -c 2 8.8.8.8
# Check if MASQUERADE rules exist
sudo nft list table ip nat | grep -i masquerade
# If missing, restart Docker to recreate them
sudo systemctl restart dockerRootless Docker uses UID namespace remapping. The container runs as UID 1000, which maps to a different UID on the host.
Check your subuid base:
grep openclaw /etc/subuid
# Example output: openclaw:165536:65536Calculate the host UID:
host_uid = subuid_base + container_uid - 1
host_uid = 165536 + 1000 - 1 = 166535
Why the -1? Rootless Docker's UID mapping reserves container UID 0 for the host user:
- Container UID 0 → Host user's actual UID (e.g., 1001 for
openclaw) - Container UID 1+ → Subordinate UID range from
/etc/subuid
So container UID 1000 maps to subuid_base + (1000 - 1) because the subuid range starts at container UID 1, not 0. You can verify this mapping inside a container:
docker run --rm openclaw:local cat /proc/self/uid_map
# 0 1001 1 <- UID 0 maps to host user (1001)
# 1 165536 65536 <- UID 1+ maps to subuid range| Container UID | Host UID | Calculation |
|---|---|---|
| 1000 (node) | Varies | subuid_base + 1000 - 1 |
Files must be owned by the calculated host UID for the container to read/write them.
If you get permission denied errors after editing config:
# Get subuid base
grep openclaw /etc/subuid | awk -F: '{print $2}' # e.g., 165536
# Calculate and fix ownership
SUBUID_BASE=$(grep openclaw /etc/subuid | awk -F: '{print $2}')
CONTAINER_UID=$(($SUBUID_BASE + 1000 - 1))
sudo chown -R $CONTAINER_UID:$CONTAINER_UID /home/openclaw/.openclaw
# Also ensure container can traverse the home directory
sudo setfacl -m u:$CONTAINER_UID:x /home/openclawNote: The container needs execute (traverse) permission on /home/openclaw to access the .openclaw subdirectory. The setfacl command grants this without changing ownership of the home directory.
sudo -u openclaw docker run --rm -v /home/openclaw/.openclaw:/home/node/.openclaw openclaw:local sh -c "id && ls -la /home/node/.openclaw"If directory shows nobody:nogroup, fix ownership using the calculation above.
# Test container connectivity
sudo -u openclaw docker run --rm alpine ping -c 2 8.8.8.8
# Check if nftables forward chain is blocking
sudo nft list chain inet filter forward
# Check if Docker's MASQUERADE rules exist
sudo nft list table ip nat | grep -i masquerade
# Restart Docker if missing
sudo systemctl restart dockerIf everything is broken:
# Stop and remove container
sudo -u openclaw docker rm -f openclaw-gateway
# Remove runtime config
sudo rm -rf /home/openclaw/.openclaw
# Re-run installer
cd /home/openclaw/rootless-docker-openclaw
sudo ./install.sh| Location | Purpose | Owner |
|---|---|---|
/home/openclaw/openclaw/ |
OpenClaw source (upstream) | openclaw |
/home/openclaw/rootless-docker-openclaw/ |
This deployment repo | openclaw |
/home/openclaw/.openclaw/ |
Runtime config and workspace | Mapped UID |
/home/openclaw/.openclaw/openclaw.json |
Gateway configuration | Mapped UID |
- Firewall allows SSH (rate-limited), HTTP/HTTPS, and OpenClaw gateway port
- All other inbound traffic dropped
- Docker forward chain explicitly allows bridge interfaces
- Using
flush table inet filterinstead offlush rulesetto preserve Docker NAT - OpenClaw runs as non-root user with UID namespace remapping
To customize firewall rules, edit configs/nftables.conf before running install.sh. Common changes:
- Add additional allowed ports in the
inputchain - Modify rate limiting rules
- Add custom logging rules
- Change the OpenClaw gateway port (default: 18789)