Skip to content

Troubleshooting

Start with the service logs; most problems name themselves there.

Terminal window
docker compose ps # what's up, what's healthy
docker compose logs --tail 100 backend worker # API and test runs
docker compose logs --tail 100 playwright-mcp # the Chromium browser service

A run that ends with status error didn’t really execute: something in the setup stopped it (the browser service, the AI provider, a quota). Its message starts with what went wrong and ends with Details: and the raw error. A failed run is a real test failure, explained in plain English.

That’s expected: nginx is published on http://localhost:8080, because port 80 is often taken (on Windows, commonly by IIS). If 8080 is taken too, publish nginx on another port in a docker-compose.override.yml:

docker-compose.override.yml
services:
nginx:
ports: !override
- "8081:80"

The frontend uses the relative path /api, so nothing else changes. Set APP_BASE_URL to the new address so links in emails and alerts are right. Other host ports: 127.0.0.1:3000 (frontend), 127.0.0.1:8000 (backend), 127.0.0.1:5433 (Postgres, change with POSTGRES_HOST_PORT), 127.0.0.1:6379 (Redis), 127.0.0.1:8931 (Chromium).

Messages about env_file (for example that an entry must be a string) or about !reset and !override tags mean Compose is too old. The compose file marks .env as optional, which needs Compose v2.24 or newer (!override needs v2.24.4). Check with docker compose version and update Docker Desktop or the Compose plugin.

The first browser runs fail right after starting

Section titled “The first browser runs fail right after starting”

The browser services install @playwright/mcp and download their browser on every start, which takes a few minutes the first time. Workers don’t wait for them, so until they’re listening, browser runs end in error with “Could not reach or drive the browser automation server”. Wait until docker compose ps shows the browser service as (healthy) instead of (health: starting), then re-run:

Terminal window
docker compose ps
docker compose logs -f playwright-mcp # or playwright-mcp-firefox, playwright-mcp-webkit

Firefox and WebKit take longer than Chromium. A service that stays (unhealthy) failed to install; its log says why (often no internet access from the container).

The backend won’t start or stays unhealthy

Section titled “The backend won’t start or stays unhealthy”

The workers and the scheduler wait for a healthy backend, so nothing runs until this is fixed. Check docker compose logs backend:

  • Refusing to start with PLAINWRIGHT_ENV=production: SECRET_KEY is …: production mode found an empty, publicly known or too-short SECRET_KEY. If the install is new, set a random key of at least 32 characters. If it already holds data encrypted with the current key, rotate the key instead: the rotation command runs in a one-off container, so the backend doesn’t need to start first.
  • A settings error such as “Input should be a valid boolean” or “…valid integer”: a number or boolean in .env is set to an empty value. Remove the line or give it a value.
  • Can't load plugin: sqlalchemy.dialects:postgres: DATABASE_URL starts with postgres://. Use postgresql://.
  • A migration error: the migration transaction rolled back, so the database is unchanged. Fix the cause in the message, then docker compose up -d backend. See Upgrades, migrations and backups.
  • module 'bcrypt' has no attribute '__about__': bcrypt was upgraded past the pinned 4.0.1. passlib 1.7.4 doesn’t work with bcrypt 4.1 or newer; keep the pin in backend/requirements.txt and rebuild.

The backend isn’t answering yet (it migrates the database on start) or has crashed. Check docker compose ps and the backend log. The bundled nginx re-resolves service addresses every 10 seconds, so a rebuilt backend is picked up without restarting nginx.

docker compose restart restarts containers with their old environment. Run docker compose up -d, which recreates the containers whose configuration changed. On hosted platforms, redeploy every service that reads the variable; Configuration lists which ones.

In development mode, the backend logs a CRITICAL line starting SECURITY: SECRET_KEY is…, and project owners and admins see the same warning as a banner in the app. The key is empty, a publicly known default, or shorter than 32 characters, so anyone who knows it can forge sign-ins and decrypt stored credentials from a database copy.

Don’t just replace the value: anything already encrypted would become unreadable. Follow Rotate SECRET_KEY, which re-encrypts stored credentials under the new key, then set PLAINWRIGHT_ENV=production so a weak key can’t come back unnoticed.

When Test connection passes but runs fail with “The AI provider rejected the API key”, the backend and the worker have different SECRET_KEY values. The backend encrypted the key with its value; the worker can’t decrypt it and sends the ciphertext as the API key. Give every service the same SECRET_KEY and recreate them (docker compose up -d). On Docker Compose they share .env, so look for a SECRET_KEY set differently on one service, or a worker started from an older environment. Right after a key rotation, it means one service still has the old key.

If SECRET_KEY was changed by hand instead of rotated, stored keys were encrypted with a key that’s gone. Re-enter each provider key in AI providers (and the vault secrets, saved logins and integration credentials in each project).

If Test connection fails too, the key itself is wrong, expired, or for a different provider. Re-enter it in AI providers.

If neither the project nor its owner has a provider and ANTHROPIC_API_KEY is empty, runs fail with an authentication error as well. Add a provider, or set the server-wide key.

“The selected AI model does not support tool/function calling”

Section titled ““The selected AI model does not support tool/function calling””

Plainwright drives everything through tool calls, so every model must support them. Test connection checks this with a no-op tool. Pick a tool-capable model, such as the current Claude, GPT and Gemini models, or llama3.1, qwen2.5 or mistral-nemo on Ollama.

“The model produced an invalid tool call”

Section titled ““The model produced an invalid tool call””

Some open models occasionally emit a malformed tool call (Llama and Qwen on Groq, for example). It’s intermittent: re-run, turn on retries, or use a more capable model.

“The AI provider rate-limited the request”

Section titled ““The AI provider rate-limited the request””

The provider answered 429. Wait and re-run, raise your plan’s limits, or run fewer tests at once: every worker makes its own AI calls. See Scaling workers.

The model’s final answer wasn’t the JSON verdict Plainwright asks for. Reasoning models (Qwen3, DeepSeek-R1 and similar) emit <think> blocks, which Plainwright strips first, but a model that spends its whole reply budget thinking leaves nothing to parse. Re-run, or choose a model that follows instructions more reliably. Very small local models are the usual cause.

Ollama models don’t show up or can’t be reached

Section titled “Ollama models don’t show up or can’t be reached”

Ollama runs on the host, not in Docker. The backend and worker reach it at http://host.docker.internal:11434:

  • Linux: Ollama listens on 127.0.0.1 by default, which containers can’t reach. Start it with OLLAMA_HOST=0.0.0.0 (and allow port 11434 from the Docker bridge in your firewall).
  • No models listed: the list comes live from the Ollama server. Pull one, from the app or with ollama pull llama3.1.
  • Runs error on a model: the model must support tool calling.

“Could not reach or drive the browser automation server”

Section titled ““Could not reach or drive the browser automation server””

The worker couldn’t use a browser service:

  • It’s still starting (see above), or it crashed: check docker compose logs playwright-mcp (or -firefox, -webkit for those browsers).
  • Its command was edited and --allowed-hosts '*' is no longer the last flag. The option takes every value after it, so flags placed after it are swallowed.
  • On a hosted platform, PLAYWRIGHT_MCP_URL doesn’t point at the browser service’s private address. Only the host and port matter; the client opens /sse on them.

Browsers need memory: about 0.5 GB per concurrent browser run. Give the browser service more memory and CPU, keep shm_size at 2 GB or more, or run fewer workers. See Scaling workers.

The agent used all 30 of its steps without finishing. Split the test into smaller ones or make the steps more specific. See Writing good tests.

No worker is taking jobs. Check that workers are running (docker compose ps worker; they only start once the backend is healthy) and can reach Redis (docker compose logs worker). If triggering a run answers “The job queue is unavailable — try again shortly” (HTTP 503), the backend can’t reach Redis.

Runs stay “running”, then end in error

Section titled “Runs stay “running”, then end in error”

“Run did not complete — its worker stopped or it exceeded the time limit (orphaned job reaped)” means the worker died or was stopped mid-run (a deploy, a crash, running out of memory), or the run took longer than 300 seconds per trial. The scheduler does this clean-up, so if it isn’t running, such runs stay running forever. Make sure exactly one scheduler runs.

Project files are stored in ./test-files/<project_id>/ and must be visible to the browser service at /test-files. The compose file mounts the folder into the backend, the workers and all three browser services; keep those mounts if you customise it. Uploads are limited to 25 MB per file. Platforms without a shared disk (Render, Railway, Fly.io) don’t support them.

“Element [ref=…] not found” and “Ambiguous element found” in a run’s steps aren’t failures by themselves. Plainwright recovers automatically by taking a fresh snapshot of the page, or by asking the agent for a more specific target. They only matter if the run then fails.

Live steps stream over a long-lived response, and a proxy that buffers responses holds them back. In your own reverse proxy, turn buffering off for /api/ (proxy_buffering off; in nginx, flush_interval -1 in Caddy) and allow long reads. See Deploy to a VPS.

  • With STORAGE_BACKEND=s3, look for “…not configured (S3_BUCKET empty) — using local storage” in the backend and worker logs; then artifacts went to the local disk instead. Both services need the S3_* settings.
  • With a private bucket, images redirect to presigned bucket URLs, so people’s browsers must be able to reach the bucket’s endpoint (a MinIO address that only resolves inside Docker won’t work).
  • Behind your own proxy, /screenshots/ must go to the backend as well as /api/.

The same guard covers API tests, eval targets, MCP servers, A2A agents, webhooks and the URLs the AI agent opens in the browser. In a browser run, a refused URL shows up in the steps as a tool error. See Security model for the full list.

“Requests to internal service ‘…’ are not allowed”

Section titled ““Requests to internal service ‘…’ are not allowed””

The URL names one of the platform’s own services (postgres, redis, backend, worker, scheduler, frontend, nginx, playwright-mcp*) or one of their container names. Point the test at the service you actually want to test.

“Requests to a loopback address are not allowed”

Section titled ““Requests to a loopback address are not allowed””

localhost, 127.0.0.1 and ::1 are refused. They’d point at the worker’s or the browser container’s own address anyway, not at your machine. To test an app running on the Docker host, use http://host.docker.internal:<port>.

“Requests to the Docker host on the platform’s own port … are not allowed”

Section titled ““Requests to the Docker host on the platform’s own port … are not allowed””

host.docker.internal is allowed, except on the ports Plainwright itself publishes: 3000, 5432, 5433, 6379, 8000, 8080 and 8931. If the app you want to test runs on one of them (a dev server on 3000, say), move it to another port.

“Requests to the platform’s internal services are not allowed”

Section titled ““Requests to the platform’s internal services are not allowed””

The host name resolves to the address of one of the platform’s services, so it’s refused like the service name itself.

“Requests to cloud metadata endpoints are not allowed”

Section titled ““Requests to cloud metadata endpoints are not allowed””

The URL is a metadata or link-local address, or a host name that resolves to one.

The URL has no scheme or uses another one. Write it in full, including https://.

On a trusted single-user install, ALLOW_INTERNAL_TARGETS=true turns all of the checks above off except the scheme check. Never set it on an install other people use.

  • No Google or GitHub button, a redirect mismatch, an expired sign-in link, or no reset or confirmation emails: see the table in Sign-in and email.
  • An invited person doesn’t see the project: their address isn’t confirmed yet. See the FAQ.
  • “Too many attempts — wait a few minutes and try again.” for everyone at once: behind the bundled nginx the backend sees one client address for all users, so the per-address sign-in limit can trip install-wide. Wait 15 minutes, and rate-limit at your edge proxy instead; see Rate limits.
  • Everyone was signed out: SECRET_KEY changed (for example after a rotation). One person was signed out on every device: a password change or reset, a sign-out of all sessions, or removal from their last project revoked their tokens. Sign in again.
  • Runs end with “Monthly run limit reached” or “Monthly AI token limit reached”: BILLING_ENABLED is true on the workers. Self-hosted installs should leave it false. See Billing.

Search the issue tracker, or open an issue with:

  • what you did, what you expected, and what happened;
  • docker compose ps and the relevant part of docker compose logs;
  • the version (git rev-parse --short HEAD) and how you deploy (Compose, Render, …).

Security problems go through private reporting, not public issues.