Deploy to a VPS
A single Linux server running the Docker Compose stack is the simplest production setup, and
the one the project tests most. You add three things to the local install: a domain with
HTTPS in front of the stack, a small override file that keeps internal ports private and
restarts containers after a reboot, and production values in .env.
Read Docker Compose first if you haven’t run the stack yet.
What you need
Section titled “What you need”- A server. 4 vCPU and 8 GB RAM run two or three browser tests at once comfortably. Any Linux that runs Docker works; the commands below are for Ubuntu 24.04 LTS.
- A domain name with a DNS
A(and optionallyAAAA) record pointing at the server, for exampleplainwright.example.com. - Ports 80 and 443 open to the internet, for HTTPS and certificate issuance.
Set up the server
Section titled “Set up the server”-
Install Docker with Docker’s convenience script, then let your user run it without
sudo.Terminal window curl -fsSL https://get.docker.com | sudo shsudo usermod -aG docker "$USER" # log out and back in afterwardsdocker compose version # must be v2.24 or newer -
Open the firewall for SSH and web traffic only.
Terminal window sudo ufw allow OpenSSHsudo ufw allow 80,443/tcpsudo ufw enable -
Get the code and create
.env.Terminal window git clone https://github.com/SaiBolisetty/Plainwright.git plainwrightcd plainwrightcp .env.example .envSet at least these values in
.env:.env # Generate with: python3 -c "import secrets; print(secrets.token_urlsafe(48))"SECRET_KEY=replace-with-a-long-random-value# Refuse to start with a missing, default or short SECRET_KEY.PLAINWRIGHT_ENV=productionAPP_BASE_URL=https://plainwright.example.comCORS_ORIGINS=https://plainwright.example.comLOG_FORMAT=json# Strongly recommended, so invitations, password resets and alerts can be emailed:SMTP_HOST=smtp.example.comSMTP_PORT=587SMTP_USER=plainwright@example.comSMTP_PASSWORD=app-password-hereSMTP_FROM=Plainwright <plainwright@example.com>Back up
.env: the key encrypts every stored provider key and secret. If you ever need to change it, rotate it rather than editing it. WithPLAINWRIGHT_ENV=productionthe backend won’t start on an empty, default or short key, so a lost.envcan’t silently fall back to the public default. See Configuration for the rest. -
Add a production override. Compose merges
docker-compose.override.ymlinto the main file automatically. This one stops publishing the frontend, binds nginx to loopback for the TLS proxy, and restarts every service after a crash or reboot.docker-compose.override.yml services:nginx:ports: !override- "127.0.0.1:8080:80" # only the TLS proxy on this host reaches itrestart: unless-stoppedfrontend:ports: !reset [] # reached through nginx onlyrestart: unless-stoppedbackend:restart: unless-stoppedworker:restart: unless-stoppedscheduler:restart: unless-stoppedpostgres:restart: unless-stoppedredis:restart: unless-stoppedplaywright-mcp:restart: unless-stoppedplaywright-mcp-firefox:restart: unless-stoppedplaywright-mcp-webkit:restart: unless-stoppedPostgres, Redis, the backend and the Chromium browser service are already published on
127.0.0.1only, so they stay private. The!overrideand!resettags need Compose v2.24.4 or newer. -
Start the stack.
Terminal window docker compose up -d --builddocker compose ps # nginx should show 127.0.0.1:8080->80/tcp -
Put HTTPS in front of it. Pick one of the two proxies below.
HTTPS with a reverse proxy
Section titled “HTTPS with a reverse proxy”The TLS proxy forwards everything to 127.0.0.1:8080. Two things matter beyond plain proxying:
don’t buffer responses (the live run view and Ollama model downloads stream over
long-lived responses), and allow uploads up to 30 MB (project test files are capped at
25 MB).
Install Caddy from its package repository, then
replace /etc/caddy/Caddyfile with:
plainwright.example.com { reverse_proxy 127.0.0.1:8080 { flush_interval -1 # stream live run events immediately } header Strict-Transport-Security "max-age=31536000; includeSubDomains"}sudo systemctl reload caddyCaddy gets and renews the certificate itself and redirects HTTP to HTTPS. It has no request-size or read-timeout limit by default.
sudo apt install -y nginx certbot python3-certbot-nginxserver { listen 80; server_name plainwright.example.com; client_max_body_size 30m;
location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; # live run streams proxy_read_timeout 3600s; # long-running streams and downloads }}sudo ln -s /etc/nginx/sites-available/plainwright /etc/nginx/sites-enabled/sudo nginx -t && sudo systemctl reload nginxsudo certbot --nginx -d plainwright.example.com # adds the 443 server and the HTTP redirectOnce HTTPS works, add
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
to the 443 server block that Certbot created.
Check the deployment
Section titled “Check the deployment”-
Open
https://plainwright.example.comand create the first account. Anyone who can reach the site can register, so do this straight away; see Security model for how to keep a private install private. -
Check that only the proxy is public. From another machine, the app’s own ports should refuse connections:
Terminal window curl -m 5 http://<server-ip>:8080/ # should failcurl -m 5 http://<server-ip>:3000/ # should failcurl -m 5 http://<server-ip>:8931/ # should fail: the browser service must never be publiccurl -I https://plainwright.example.com/ # 200, with strict-transport-security and x-frame-optionsOn the server,
docker compose psshould show127.0.0.1:in front of every published port. -
Run a test. The first browser run after a restart can take a little longer while the browser services finish installing.
Keep it running
Section titled “Keep it running”- Backups. Schedule a nightly
pg_dumpand keep a copy of.env, thescreenshotsvolume and./test-filesoff the server. See Upgrades, migrations and backups. - Updates.
git pull && docker compose up -d --build. Migrations run when the backend starts. - Monitoring.
LOG_FORMAT=jsonfor log shippers,SENTRY_DSNfor errors, and Prometheus on127.0.0.1:8000/metrics. See Monitoring. - Disk. Screenshots accumulate under the
screenshotsvolume. Move them to object storage with a lifecycle rule if disk is tight.
Next steps
Section titled “Next steps”- Sign-in and email: Google and GitHub sign-in, and SMTP.
- Scaling workers: more parallel runs on a bigger server.
- GitHub Actions: run your tests on every deploy.