Setup OpenClaw on Fedora 43 with Podman Quadlets + Tailscale
The quirks are worth it...
Overview
OpenClaw can runs as a rootless Podman container managed by a systemd Quadlet under a dedicated openclaw user on Fedora Workstation 43. I’m running it this way on a remote server, and I wanted Tailscale access to the OpenClaw gateway dashboard.
The gateway binds to loopback only, and Tailscale Serve (running on the host) provides HTTPS access across the tailnet at https://my-device.<tailnet>.ts.net/. A wrapper script at /usr/local/bin/openclaw lets me run CLI commands without the verbose sudo/podman exec boilerplate.
Quirks I encountered, and fixes
1. SELinux blocking container access to mounted volumes
The container couldn’t read its own config files. The setup wizard and gateway both failed with EACCES errors:
Failed to read config at /home/node/.openclaw/openclaw.json Error: EACCES: permission denied, open '/home/node/.openclaw/openclaw.json'
at Object.readFileSync (node:fs:440:20)
at Object.loadConfig (file:///app/dist/model-selection-DzQRvJ4C.js:10141:24)
The fix was adding :Z to all volume mounts so Podman relabels the files with the correct SELinux context. This needed to be applied in two places:
- The launch script (
run-openclaw-podman.sh):-v "$CONFIG_DIR:/home/node/.openclaw:rw,Z" - The Quadlet file (
openclaw.container):Volume=/home/openclaw/.openclaw:/home/node/.openclaw:Z
2. Gateway refusing non-loopback connections
With --bind lan or --bind tailnet, the gateway requires either explicit controlUi.allowedOrigins or a secure HTTPS context. Plain HTTP over a Tailscale IP also triggered “device identity required” errors since the browser lacked a secure context.
The solution was to keep the gateway on --bind loopback and use tailscale serve on the host to provide HTTPS.
3. Container networking with pasta
The Quadlet’s default pasta networking meant port 18789 was only accessible within the openclaw user’s network namespace, not directly on the host loopback. tailscale serve couldn’t reach it.
Adding Network=host to the Quadlet fixed this, allowing the container to bind directly on the host’s loopback interface.
4. Tailscale Serve for HTTPS
Rather than containerizing Tailscale, we used the host’s existing Tailscale installation:
sudo tailscale serve --bg 18789
This proxies https://my-device.<tailnet>.ts.net/ to localhost:18789 with automatic HTTPS via Tailscale certs. The config persists across reboots.
5. Trusted proxies and pairing errors
Tailscale Serve forwards requests from loopback, but the gateway didn’t trust the proxy headers and treated all connections as remote, demanding device pairing (WebSocket close code 1008).
Two config additions fixed this:
trustedProxies: ["127.0.0.1/8"]ā tells the gateway to trust proxy headers from loopbackauth.allowTailscale: trueā enables Tailscale identity-based authentication for the Control UI
6. Token mismatch between .env and openclaw.json
The CLI couldn’t authenticate with the gateway, failing with “gateway token mismatch.” The root cause was that setup-podman.sh generated one token and wrote it to ~openclaw/.openclaw/.env, and then the onboarding wizard generated a different token and wrote it to openclaw.json. The OPENCLAW_GATEWAY_TOKEN environment variable silently overrides the config file token, so the gateway was using the .env value while the config showed a different one. The fix was updating the .env to match the token in openclaw.json and restarting the service:
sudo -u openclaw sed -i 's/OPENCLAW_GATEWAY_TOKEN=.*/OPENCLAW_GATEWAY_TOKEN=<your-token>/' /home/openclaw/.openclaw/.env
sudo systemctl --machine openclaw@ --user restart openclaw.service
7. CLI wrapper script
Running commands inside the container required a long sudo -u openclaw XDG_RUNTIME_DIR=... podman exec invocation that also failed with permission denied if your current directory wasn’t accessible to the openclaw user.
Created /usr/local/bin/openclaw:
#!/bin/bash
(cd / && exec sudo -u openclaw XDG_RUNTIME_DIR=/run/user/$(id -u openclaw) podman exec -it openclaw node dist/index.js "$@")
And then, of course, run sudo chmod +x /usr/local/bin/openclaw.
The subshell with cd / avoids the permission error without changing your actual working directory. Usage:
openclaw devices list
openclaw doctor
openclaw nodes status
Final configuration
Quadlet (~openclaw/.config/containers/systemd/openclaw.container)
[Unit]
Description=OpenClaw gateway (rootless Podman)
[Container]
Image=openclaw:local
ContainerName=openclaw
UserNS=keep-id
User=%U:%G
Network=host
Volume=/home/openclaw/.openclaw:/home/node/.openclaw:Z
EnvironmentFile=/home/openclaw/.openclaw/.env
Environment=HOME=/home/node
Environment=TERM=xterm-256color
Pull=never
Exec=node dist/index.js gateway --bind loopback --port 18789
[Service]
TimeoutStartSec=300
Restart=on-failure
[Install]
WantedBy=default.target
Gateway config (~openclaw/.openclaw/openclaw.json, gateway block)
{
"gateway": {
"port": 18789,
"mode": "local",
"bind": "loopback",
"trustedProxies": ["127.0.0.1/8"],
"controlUi": {
"allowedOrigins": [
"http://localhost:18789",
"http://127.0.0.1:18789",
"https://my-device.<tailnet>.ts.net"
]
},
"auth": {
"mode": "token",
"token": "<your-token>",
"allowTailscale": true
},
"tailscale": {
"mode": "off",
"resetOnExit": false
}
}
}
Service management
# Start/stop/restart
sudo systemctl --machine openclaw@ --user start openclaw.service
sudo systemctl --machine openclaw@ --user stop openclaw.service
sudo systemctl --machine openclaw@ --user restart openclaw.service
# Status and logs
sudo systemctl --machine openclaw@ --user status openclaw.service
sudo -u openclaw XDG_RUNTIME_DIR=/run/user/$(id -u openclaw) journalctl --user -u openclaw.service -f