Deploy to Railway
On Railway, each part of Plainwright becomes its own service in one project, built from your repository or from a public image, and the services reach each other over Railway’s private network. Use Railway when you want a managed platform with per-service scaling and no blueprint file. As on any platform without a shared disk, screenshots go to object storage and project file uploads aren’t available.
The services
Section titled “The services”| Service | Source | Start command | Notes |
|---|---|---|---|
Postgres |
Railway’s PostgreSQL template | — | Provides DATABASE_URL. |
Redis |
Railway’s Redis template | — | Provides REDIS_URL. Must not evict keys. |
backend |
Repo, root directory backend |
uvicorn app.main:app --host :: --port 8000 |
Migrates the database on start. Private only. |
worker |
Repo, root directory backend, Dockerfile Dockerfile.worker |
rq worker test_runs |
Replicas = parallel runs. |
scheduler |
Repo, root directory backend |
python -m app.services.scheduler |
Exactly one replica. |
playwright-mcp |
Image mcr.microsoft.com/playwright:v1.44.0-jammy |
see step 5 | Headless Chromium. Give it at least 2 GB of RAM. |
web |
Repo root, Dockerfile deploy/web/Dockerfile |
— | Frontend + reverse proxy. The only public service. |
Railway’s private network may be IPv6-only, so every service that others call listens on ::.
Add the web service files
Section titled “Add the web service files”The frontend calls the API at the relative path /api, so the UI and the API must share one
origin. Add a small image to your copy of the repository that serves the frontend build and
proxies /api/ and /screenshots/ to the backend, like nginx/nginx.conf does in Docker
Compose:
Directorydeploy
Directoryweb
- Dockerfile
- default.conf.template
Directorybackend/
- …
Directoryfrontend/
- …
# Frontend build + reverse proxy, so the UI and the API share one public origin.# Build context: the repository root.FROM node:20-alpine AS buildWORKDIR /appCOPY frontend/package*.json ./RUN npm ciCOPY frontend/ ./RUN npm run build
FROM nginx:alpineCOPY --from=build /app/dist /usr/share/nginx/htmlCOPY deploy/web/default.conf.template /etc/nginx/templates/default.conf.template# Makes the entrypoint export NGINX_LOCAL_RESOLVERS (the nameservers in /etc/resolv.conf).ENV NGINX_ENTRYPOINT_LOCAL_RESOLVERS=1# The nginx image fills in ${...} from the environment when the container starts.server { listen 8080; server_tokens off; client_max_body_size 30m; root /usr/share/nginx/html;
# Re-resolve the backend's private address, so a redeployed backend doesn't cause 502s. resolver ${NGINX_LOCAL_RESOLVERS} valid=10s;
add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location = /metrics { return 404; }
location /api/ { set $backend ${BACKEND_HOST}; proxy_pass http://$backend$request_uri; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_buffering off; # live run streams proxy_read_timeout 3600s; }
location /screenshots/ { set $backend ${BACKEND_HOST}; proxy_pass http://$backend$request_uri; }
location / { try_files $uri $uri/ /index.html; }}BACKEND_HOST is the backend’s private host and port, set on the web service in step 6.
Deploy
Section titled “Deploy”-
Create a project and add PostgreSQL and Redis from Railway’s templates.
-
Add shared variables (project settings → Shared Variables). Generate
SECRET_KEYonce; it must be identical on every app service. To change it later, use the rotation command, never an edit in place.Variable Value SECRET_KEYA random value of at least 32 characters: python3 -c "import secrets; print(secrets.token_urlsafe(48))"PLAINWRIGHT_ENVproduction, so the backend refuses to start with a missing or weak keyAPP_BASE_URL,CORS_ORIGINSThe webservice’s public URL, e.g.https://plainwright.up.railway.app(fill in after step 6)STORAGE_BACKENDs3S3_ENDPOINT_URL,S3_REGION,S3_BUCKET,S3_ACCESS_KEY_ID,S3_SECRET_ACCESS_KEYYour bucket; see Object storage LOG_FORMATjson -
Add the
backendservice from your repository. In its settings, set the root directory tobackendand the custom start command touvicorn app.main:app --host :: --port 8000. Give it these variables:backend variables DATABASE_URL=${{Postgres.DATABASE_URL}}REDIS_URL=${{Redis.REDIS_URL}}PLAYWRIGHT_MCP_URL=http://playwright-mcp.railway.internal:8931/mcpSECRET_KEY=${{shared.SECRET_KEY}}PLAINWRIGHT_ENV=${{shared.PLAINWRIGHT_ENV}}APP_BASE_URL=${{shared.APP_BASE_URL}}CORS_ORIGINS=${{shared.CORS_ORIGINS}}STORAGE_BACKEND=${{shared.STORAGE_BACKEND}}S3_ENDPOINT_URL=${{shared.S3_ENDPOINT_URL}}S3_REGION=${{shared.S3_REGION}}S3_BUCKET=${{shared.S3_BUCKET}}S3_ACCESS_KEY_ID=${{shared.S3_ACCESS_KEY_ID}}S3_SECRET_ACCESS_KEY=${{shared.S3_SECRET_ACCESS_KEY}}LOG_FORMAT=${{shared.LOG_FORMAT}}Don’t give it a public domain;
webreaches it privately. -
Add
workerandschedulerthe same way, with the same variables.worker: root directorybackend, variableRAILWAY_DOCKERFILE_PATH=Dockerfile.worker, start commandrq worker test_runs, andRQ_REDIS_URL=${{Redis.REDIS_URL}}so therqcommand finds Redis. Set the replica count to the number of tests to run at once.scheduler: root directorybackend, start commandpython -m app.services.scheduler, one replica.
-
Add
playwright-mcpfrom the Docker imagemcr.microsoft.com/playwright:v1.44.0-jammy, with this start command. It’s the Docker Compose command with--host ::, and--allowed-hostsmust stay last:Terminal window bash -c "npm install -g @playwright/mcp@latest && npx --package=@playwright/mcp@latest -c 'playwright install chromium' && npx @playwright/mcp@latest --port 8931 --host :: --headless --no-sandbox --browser chromium --isolated --allowed-hosts '*'"Name the service exactly
playwright-mcpsoplaywright-mcp.railway.internalresolves, and don’t give it a public domain: the MCP server has no authentication. -
Add
webfrom your repository with the root directory left at the repository root,RAILWAY_DOCKERFILE_PATH=deploy/web/DockerfileandBACKEND_HOST=backend.railway.internal:8000. Generate a public domain for it on port8080, then put that URL into theAPP_BASE_URLandCORS_ORIGINSshared variables and redeploy the app services. -
Open the
webURL and create the first account.
Operating on Railway
Section titled “Operating on Railway”- Scaling. Raise the
workerreplica count for more parallel runs, and giveplaywright-mcpmore memory to match (about 0.5 GB per concurrent browser run). See Scaling workers. - Redis eviction. The queue must never drop keys. Check with
redis-cli CONFIG GET maxmemory-policyand setnoevictionif it isn’t already. - Updates. Pushing to the connected branch redeploys every service built from the repository. The backend migrates the database when it starts.
- Firefox and WebKit. Add
playwright-mcp-firefox/playwright-mcp-webkitservices with--browser firefoxor--browser webkit(WebKit needsplaywright install --with-deps webkit) and setPLAYWRIGHT_MCP_URL_FIREFOX/PLAYWRIGHT_MCP_URL_WEBKITon the worker. - Postgres URL scheme. Plainwright needs
postgresql://. If a database gives youpostgres://, change the scheme.
Next steps
Section titled “Next steps”- Configuration: sign-in, email and every other setting.
- Monitoring: JSON logs, Sentry and
/metrics. - CI webhook and gates: run tests after each deploy.