Why OpenClaw's Browser Breaks on Your VPS (and How to Fix It for Good)
“Having huge troubles with my VPS. Reinstalling OpenClaw for the 3rd or 4th time now.”
That message appeared in the OpenClaw Discord last month. The user wasn’t alone. Scroll through the community channels and you’ll find dozens of variations on the same theme: browser tool fails, agent can’t reach Chromium, VPS runs out of memory, everything worked yesterday and now it doesn’t.
If you’re reading this because you just searched “OpenClaw browser not working VPS,” you’re in the right place. This post explains the five things that break, why they interact in frustrating ways, and how we solved all of them in the OpenClaw.rocks Kubernetes operator so you never have to think about browser setup again.
The error everyone sees
The most common error message in the OpenClaw community is this one:
Can't reach the OpenClaw browser control service
(timed out after 15000ms)
It shows up on Ubuntu, Debian, in Docker, on Hetzner, Hostinger, GCP, and everywhere else people try to run OpenClaw with browser automation. The gateway logs say “Browser control service ready.” But when the agent tries to use the browser, it times out.
The error is generic. The causes are not. There are at least five distinct failure modes, and they look identical from the outside.
Failure 1: Snap Chromium and the AppArmor wall
On a fresh Ubuntu 22.04+ server, which chromium-browser returns /usr/bin/chromium-browser. That looks correct. It is not.
Since Ubuntu 22.04, the default Chromium package is a snap. When OpenClaw’s gateway tries to spawn that binary through a systemd service, AppArmor’s confinement layer blocks it. The snap package can’t bind Chrome DevTools Protocol (CDP) debugging ports through the sandbox. The binary launches, appears to start, then silently fails to open the port OpenClaw needs.
You’ll see “Failed to start Chrome CDP on port 18800” in the logs, or sometimes nothing at all. The browser process starts and dies before it writes a single log line.
The fix is to install Google Chrome’s .deb package or use Playwright’s standalone Chromium binary from ~/.cache/ms-playwright/. Both bypass the snap sandbox. But the fix isn’t obvious. OpenClaw’s browser detection scans standard paths in order, finds the snap binary first, and tries to use it. GitHub issue #4978 has a full writeup.
Failure 2: Headless defaults that assume a display
OpenClaw ships with headless: false and noSandbox: false as browser defaults. This makes sense on a Mac or Windows machine with a display. On a VPS, there is no display. Without explicit configuration, Chromium tries to open a window on a display server that doesn’t exist.
Two config changes fix it:
openclaw config set browser.headless true
openclaw config set browser.noSandbox true
Most VPS guides mention this. But if you’re following the standard OpenClaw install instructions, neither line appears. You install, you try the browser, it fails. You search the error. You find a blog post. You add the config. You restart. Now you’re on to failure number three.
Failure 3: The OOM kill you can’t see
A single Chromium tab with a few open pages consumes 2 to 4 GB of RAM. On a 4 GB VPS, that leaves nearly nothing for OpenClaw itself, the operating system, and any other services you’re running.
When Linux runs out of memory, the kernel’s OOM killer terminates the most memory-hungry process. That’s Chromium. The process dies. OpenClaw logs a browser timeout. There’s no crash report, no stack trace, no indication that memory was the issue.
There’s a subtler version of this problem too. Chromium spawns child renderer processes for each tab. If those processes aren’t cleaned up properly, they accumulate. One GitHub issue documented 39 orphaned renderer processes consuming 3.8 GB of RAM on a VPS that was supposed to have plenty of headroom.
You can check after the fact with dmesg | grep -i "oom\|killed\|chromium", but most people don’t think to look there. They restart OpenClaw, it works for a few minutes, and then it happens again.
The official server requirements page recommends 4 GB minimum for a basic setup. But that number assumes no browser automation. With the browser enabled, 8 GB is the realistic floor. On Hetzner, that’s the difference between a cpx22 ($5/mo) and a cpx42 ($17/mo).
Failure 4: Docker’s missing shared memory
If you run OpenClaw in Docker (common for VPS deployments), there’s another trap. Docker’s default /dev/shm is 64 MB. Chromium uses shared memory for inter-process communication. When it runs out, tabs crash silently or render blank pages.
The fix is one line in your docker-compose.yml:
shm_size: '2gb'
Or mount it explicitly:
volumes:
- /dev/shm:/dev/shm
This isn’t documented in OpenClaw’s setup guide. It’s a Docker-specific behavior that affects any application using Chromium, but unless you’ve deployed headless browsers in Docker before, you wouldn’t know to look for it.
Failure 5: Port collisions nobody warned you about
OpenClaw’s browser control service runs on a separate port from the gateway. By default, it’s the gateway port plus 2. If your gateway is on 18789, the browser control service should be on 18791, and the extension relay on 18792.
In Docker, if you only expose port 18789, the browser control service is unreachable from outside the container. Worse, the gateway logs “Browser control service ready” even when port 18791 never actually binds. Issue #17584 traced this to the gateway importing the wrong module: one that logs the “ready” message without starting the HTTP server. The log tells you everything is fine. The port is dead.
In Kubernetes, if another container in the same pod already uses port 9222 (the standard CDP port), OpenClaw’s ensurePortAvailable() function detects the port as occupied and refuses to connect, even if the occupant is the exact Chromium instance OpenClaw should be talking to.
GitHub issue #10994 documents a case where the user’s VPS setup worked correctly. Then, after a few automated actions, the CDP port got stuck. The only fix was finding and killing stray Chromium processes holding the port.
Why these failures compound
Each of these five problems has a known fix. But they interact. You solve the snap issue and hit the headless default. You fix the config and Chromium starts. It runs for ten minutes, then the OOM killer takes it down. You add swap space, and now Docker’s shared memory limit causes silent rendering failures. You fix that and discover port 18791 isn’t exposed.
The debugging loop looks like this: search error, find partial fix, apply fix, hit next error, repeat. One Discord user described reinstalling their entire OS “for the 3rd or 4th time.” Another reported that the browser was “unstable” after what they thought was a complete setup. A third opened an issue titled simply “Browser tool consistently fails on VPS.”
The problem isn’t that any individual fix is hard. The problem is that there are five of them, they depend on your specific OS, package manager, container runtime, and VPS provider, and one mistake in the chain means the browser doesn’t work.
To their credit, the OpenClaw team has been fixing related bugs steadily. The misleading “ready” log, the orphaned renderer cleanup, and the CDP port handling have all improved in recent versions. But the upstream fixes address symptoms inside OpenClaw’s code. They can’t install the right Chromium binary on your server, configure your Docker shared memory, or allocate enough RAM for browser automation. Those remain your responsibility.
How we broke Chromium three times (and what we learned)
We run every OpenClaw.rocks agent on Kubernetes using our open-source operator. The browser sidecar took us weeks to get right. We hit our own version of every problem described above, plus a few that only show up in a container orchestration context.
The idea was simple: run Chromium as a separate container next to OpenClaw, connected over CDP. One line in the custom resource and the browser just works.
spec:
chromium:
enabled: true
Here’s how that one line actually came together.
Crash 1: Wrong user, read-only filesystem
Our first attempt ran the Chromium container as UID 1001 with a read-only root filesystem. Security best practice. Also completely broken. The browserless image expects UID 999 (blessuser), and Node.js calls os.userInfo() on startup, which fails with ENOENT when the UID has no /etc/passwd entry. Even after fixing the UID, the read-only filesystem blocked Chrome from writing temp files. The sidecar crashed on every startup before writing a single log line. Issue #12 has the full story.
Crash 2: The WebSocket security wall
With the container finally running, Chromium was up, CDP responded on port 9222, but OpenClaw refused to use it. The gateway was binding to the pod’s LAN IP (required for Kubernetes Service routing), and OpenClaw’s security layer blocked plaintext ws:// connections to non-loopback addresses: “SECURITY ERROR: Gateway URL uses plaintext ws:// to a non-loopback address. Both credentials and chat data would be exposed to network interception.”
A legitimate security check. But in a Kubernetes pod, all containers share a network namespace. Traffic between them never leaves the node. We couldn’t change OpenClaw’s security check, so we added an nginx reverse proxy sidecar that listens on all interfaces and forwards to the gateway on loopback. The gateway stays secure. The Service stays reachable. Issue #135 documents the debugging.
Crash 3: Port 3000 collision
Chromium was running. The gateway was proxied. But the browser still wouldn’t connect. The browserless image defaults to port 3000, which collided with OpenClaw’s internal browser control service. And when we switched to port 9222, OpenClaw’s ensurePortAvailable() detected it as “in use by something else” and refused to connect, because in a shared network namespace, the sidecar’s port looks local.
The fix was using the pod’s IP address (via Kubernetes Downward API) instead of localhost for the CDP URL. A non-loopback address tells OpenClaw to use remote/attach-only mode: connect to what’s already running instead of trying to launch its own browser. PR #183 solved this.
The result: five fixes, applied automatically
Each of these crashes taught us something. The current operator encodes all of it:
Sidecar isolation. Chromium runs in its own container. No snap. No Playwright. No headless flags. The browserless image handles all of that.
Dedicated resources. The sidecar gets its own CPU and memory limits (250m-1000m CPU, 512Mi-2Gi RAM by default, configurable). If Chromium exceeds its memory limit, Kubernetes restarts only the browser container. Your agent keeps running.
Shared memory. The operator mounts a 1 GB memory-backed volume at /dev/shm automatically. No Docker shm_size configuration needed.
Port routing. Pod IP for the CDP URL activates remote mode. OpenClaw connects to the sidecar instead of trying to claim the port. The ensurePortAvailable() conflict disappears.
Anti-bot detection built in. Many websites flag default headless Chromium and block automation. The operator supports extraArgs on the Chromium sidecar, so you can pass flags like --disable-blink-features=AutomationControlled and custom user agents without building a custom container image:
spec:
chromium:
enabled: true
extraArgs:
- "--disable-blink-features=AutomationControlled"
- "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
- "--window-size=1920,1080"
Security without tradeoffs. All Linux capabilities dropped, privilege escalation disabled, seccomp RuntimeDefault, non-root UID 999. No --no-sandbox as root.
What this means for you
If you’re running OpenClaw on a VPS and the browser works, congratulations. You’ve navigated five distinct failure modes and emerged on the other side. Keep your setup. Back up your config.
If the browser doesn’t work, or if it works intermittently, or if you’re tired of debugging Chromium on a $5 VPS, consider what your time is worth.
OpenClaw.rocks runs every agent on Kubernetes with the operator described above. The browser sidecar is pre-configured. Memory is allocated. Ports are routed. Security is hardened. You don’t install anything, configure anything, or debug anything.
Your agent gets a browser that works. Every time. Out of the box.
The five fixes, summarized
If you want to stay self-hosted, here’s the complete checklist:
- Replace snap Chromium with Google Chrome
.debor Playwright’s standalone binary - Enable headless mode:
openclaw config set browser.headless trueandbrowser.noSandbox true - Allocate 8 GB+ RAM or add 4 GB swap for browser automation
- Mount shared memory in Docker:
shm_size: '2gb' - Expose all three ports: gateway, browser control (gateway + 2), and extension relay (gateway + 3)
Or skip the checklist. Get your assistant on OpenClaw.rocks and let us handle the infrastructure.