Skip to content

Steel Browser v0.5.1-beta: Open Source AI Browser Setup

Install Steel Browser v0.5.1-beta, the open source AI browser API for agents. Docker setup, real RAM budgets, M1 fix, and verification in under 10 minutes.

7 min readIntermediate

Two ways to run an open source AI browser locally: pull the combined ghcr.io/steel-dev/steel-browser image and have everything – API, UI, Chromium – in one container, or use the split steel-browser-api + steel-browser-ui compose file and run them as separate services. The combined image wins for almost everyone. Fewer moving parts, one port to expose, UI auto-mounted at /ui. The split setup only makes sense if you’re putting the UI behind a different domain or scaling API replicas independently – and choosing the wrong one costs you a port binding and an hour of confusion.

This tutorial covers Steel Browser v0.5.1-beta, published November 19, 2025 (GitHub releases page). Apache 2.0 licensed, 6.2k stars as of that date, headless browser API built for AI agents – Puppeteer/Playwright on the inside, REST + WebSocket on the outside. An independent review nailed the pitch in one line: Humans use Chrome, Agents use Steel.

Be honest about RAM first

RAM is the real ceiling here – not CPU. Each active browser session consumes ~300-500 MB depending on page complexity, per Steel’s Railway template documentation. A 4 GB VM hits its limit around 8 concurrent sessions before swap kicks in. Plan accordingly before you pick a host.

Tier CPU RAM Disk Realistic concurrency
Minimum (1 session) 2 vCPU 2 GB 10 GB 1-2 sessions
Recommended (dev) 4 vCPU 4 GB 20 GB 6-8 sessions
Small production 8 vCPU 8 GB 40 GB ~15 sessions

The container ships with Chrome 128.0.6613.119 and Node 22.13.0 baked in (as of v0.5.1-beta per Steel’s official docs), so you don’t install those yourself unless you’re going bare-metal. Linux x86_64 is the smoothest host – Apple Silicon has a known wrinkle covered below.

Pull the image

docker pull ghcr.io/steel-dev/steel-browser:latest

For a repeatable deployment, pin to the v0.5.1-beta tag instead of :latest. The project is in public beta – independent reviewers have flagged occasional breaking changes between minor versions. Lock it down if stability matters.

Run it

One command, persistent log storage included:

docker run -d 
 --name steel-browser 
 -p 3000:3000 
 -p 9223:9223 
 -v steel-data:/data 
 -e LOG_STORAGE_ENABLED=true 
 -e LOG_STORAGE_PATH=/data/browser-logs.duckdb 
 ghcr.io/steel-dev/steel-browser:latest

Port 3000 serves the REST API and the UI at http://localhost:3000/ui. Port 9223 is the Chrome DevTools Protocol socket. Steel’s docs carry an explicit warning: never expose 9223 to the public internet. Anyone who reaches it gets full CDP control of your Chrome instance – the Railway template intentionally keeps it private-network-only for exactly this reason.

Split services with Docker Compose

UI on its own port, API separate:

services:
 api:
 image: ghcr.io/steel-dev/steel-browser-api:latest
 ports:
 - "3000:3000"
 - "9223:9223"
 environment:
 - DOMAIN=localhost:3000
 - CDP_DOMAIN=localhost:9223
 - LOG_STORAGE_ENABLED=true
 - LOG_STORAGE_PATH=/data/logs/browser-logs.duckdb
 volumes:
 - logs:/data/logs
 ui:
 image: ghcr.io/steel-dev/steel-browser-ui:latest
 ports:
 - "5173:80"
 environment:
 - API_URL=http://api:3000
volumes:
 logs:

docker compose up -d. UI on 5173, API on 3000.

Configuration variables worth knowing

Self-hosted Steel has no API key and no auth by default – you’re the gatekeeper. The env vars you’ll actually touch:

  • DOMAIN – the public hostname the API reports back in session URLs. Running on a VPS? Set this to your real domain so live viewer links resolve correctly.
  • CDP_DOMAIN – same idea, for the WebSocket endpoint Puppeteer/Playwright connect to.
  • LOG_STORAGE_ENABLED=true – activates the DuckDB log store. DuckDB is embedded and zero-dependency, which is why Steel chose it over Postgres for session logs. Without this flag, browser logs vanish when the container restarts.
  • CHROME_EXECUTABLE_PATH – bare-metal only, when Chrome isn’t in a standard lookup path.

For production: reverse proxy (Caddy or nginx) in front of port 3000, auth at that layer. Steel itself trusts whoever can reach the port.

Verify the install

Three checks, in order:

  1. Health endpoint:curl http://localhost:3000/v1/health → should return {"status":"ok"}.
  2. Swagger UI:http://localhost:3000/documentation – full REST reference, interactive.
  3. Create a session:
    curl -X POST http://localhost:3000/v1/sessions 
     -H "Content-Type: application/json" 
     -d '{"blockAds": true, "dimensions": {"width": 1280, "height": 800}}'

    Response includes a session ID, a websocketUrl, and a sessionViewerUrl you can open in your browser to watch the agent live.

Connect Puppeteer in two lines:

import puppeteer from "puppeteer-core";
const browser = await puppeteer.connect({ browserWSEndpoint: "ws://localhost:3000" });

Three install traps

1. Apple Silicon: nginx io_setup failure.GitHub issue #106 captures this exactly: nginx: [emerg] io_setup() failed (38: Function not implemented). The API container reports Chrome starts fine, but CDP websocket connections die immediately. Why? The io_setup syscall is part of Linux’s async I/O interface – macOS’s kernel doesn’t expose it, and QEMU emulation (which Docker Desktop uses on M1) doesn’t implement it either. Forcing the amd64 image sidesteps the emulation layer entirely:

docker build --platform linux/amd64 .
# or in compose: add platform: linux/amd64 under each service

2. Port 3000 already in use. Node devs love that port. Kill what’s there (lsof -i :3000) or remap with -p 3001:3000. If you remap, the UI inside the container still expects to reach the API on internal port 3000 – set API_URL accordingly in the split-compose setup or the session viewer links will break.

3. “Chrome executable not found” on bare-metal. Only hits people who skipped Docker. Steel checks standard paths; if yours isn’t one of them, set CHROME_EXECUTABLE_PATH=/path/to/chrome before running npm run dev.

Stealth caveat: Steel handles standard bot checks, but independent testing confirms Cloudflare Turnstile and PerimeterX still fingerprint it. Built-in anti-detection isn’t a silver bullet. For those targets, pair Steel with a residential proxy via the proxyUrl session option and budget for retries.

Which raises a fair question for anyone building multi-tenant agent infrastructure: how does Steel’s session isolation actually hold under load? The docs describe per-session Chrome profiles, but what happens when 15 sessions hit the same host simultaneously and one crashes – does the container recover gracefully or do you lose the other 14? That behavior isn’t documented, and it’s worth testing before you commit to a production architecture.

Upgrade and uninstall

Upgrades are boring, which is the point:

docker pull ghcr.io/steel-dev/steel-browser:latest
docker stop steel-browser && docker rm steel-browser
# re-run your original docker run command

The steel-data volume persists logs and Chrome profile cache across restarts. Check the Steel docs for release notes before bumping – minor versions have shipped breaking API changes before.

Full uninstall:

docker stop steel-browser
docker rm steel-browser
docker volume rm steel-data
docker rmi ghcr.io/steel-dev/steel-browser:latest

Split compose users: docker compose down -v nukes containers and named volumes in one shot.

FAQ

Is Steel Browser actually free if I self-host?

Yes. Apache 2.0 – no license fees, no per-session charge, no usage meter. You pay for the box it runs on, nothing else. (The hosted version at steel.dev offers 100 hours/month free and paid plans starting at $29/month as of late 2025, but that’s a separate product.)

Can I use Steel with Playwright or Selenium instead of Puppeteer?

All three work. Create a session via POST /v1/sessions, grab the websocketUrl from the response, then connect: Playwright uses browser.connect_over_cdp(), Puppeteer uses puppeteer.connect(). Selenium is the odd one out – Steel exposes a drop-in WebDriver endpoint at /selenium, but that path skips CDP-only features like the live session viewer. If your team is already on Selenium, it works; just know you’re missing the debug tooling that makes Steel worth running in the first place.

Why not just run Puppeteer in Docker myself?

For a single-purpose scraper, lighter is better – raw Puppeteer-in-Docker is the right call. Steel’s overhead pays off the moment you need persistent sessions across separate API calls, a live debug viewer, proxy rotation, CAPTCHA hooks, or multi-tenant session isolation. That’s weeks of plumbing. The Steel image gives you a REST API for all of it on day one. If you don’t need those features yet, wait until you do – then switch.

Start with docs.steel.dev, grab the Steel CLI (curl -fsS https://setup.steel.dev | sh), and wire your first agent to the local instance. One session, live viewer open, watch it run. Then scale.