Skip to content

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.

  • 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 optionally AAAA) record pointing at the server, for example plainwright.example.com.
  • Ports 80 and 443 open to the internet, for HTTPS and certificate issuance.
  1. Install Docker with Docker’s convenience script, then let your user run it without sudo.

    Terminal window
    curl -fsSL https://get.docker.com | sudo sh
    sudo usermod -aG docker "$USER" # log out and back in afterwards
    docker compose version # must be v2.24 or newer
  2. Open the firewall for SSH and web traffic only.

    Terminal window
    sudo ufw allow OpenSSH
    sudo ufw allow 80,443/tcp
    sudo ufw enable
  3. Get the code and create .env.

    Terminal window
    git clone https://github.com/SaiBolisetty/Plainwright.git plainwright
    cd plainwright
    cp .env.example .env

    Set 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=production
    APP_BASE_URL=https://plainwright.example.com
    CORS_ORIGINS=https://plainwright.example.com
    LOG_FORMAT=json
    # Strongly recommended, so invitations, password resets and alerts can be emailed:
    SMTP_HOST=smtp.example.com
    SMTP_PORT=587
    SMTP_USER=plainwright@example.com
    SMTP_PASSWORD=app-password-here
    SMTP_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. With PLAINWRIGHT_ENV=production the backend won’t start on an empty, default or short key, so a lost .env can’t silently fall back to the public default. See Configuration for the rest.

  4. Add a production override. Compose merges docker-compose.override.yml into 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 it
    restart: unless-stopped
    frontend:
    ports: !reset [] # reached through nginx only
    restart: unless-stopped
    backend:
    restart: unless-stopped
    worker:
    restart: unless-stopped
    scheduler:
    restart: unless-stopped
    postgres:
    restart: unless-stopped
    redis:
    restart: unless-stopped
    playwright-mcp:
    restart: unless-stopped
    playwright-mcp-firefox:
    restart: unless-stopped
    playwright-mcp-webkit:
    restart: unless-stopped

    Postgres, Redis, the backend and the Chromium browser service are already published on 127.0.0.1 only, so they stay private. The !override and !reset tags need Compose v2.24.4 or newer.

  5. Start the stack.

    Terminal window
    docker compose up -d --build
    docker compose ps # nginx should show 127.0.0.1:8080->80/tcp
  6. Put HTTPS in front of it. Pick one of the two proxies below.

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:

/etc/caddy/Caddyfile
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"
}
Terminal window
sudo systemctl reload caddy

Caddy gets and renews the certificate itself and redirects HTTP to HTTPS. It has no request-size or read-timeout limit by default.

  1. Open https://plainwright.example.com and 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.

  2. 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 fail
    curl -m 5 http://<server-ip>:3000/ # should fail
    curl -m 5 http://<server-ip>:8931/ # should fail: the browser service must never be public
    curl -I https://plainwright.example.com/ # 200, with strict-transport-security and x-frame-options

    On the server, docker compose ps should show 127.0.0.1: in front of every published port.

  3. Run a test. The first browser run after a restart can take a little longer while the browser services finish installing.

  • Backups. Schedule a nightly pg_dump and keep a copy of .env, the screenshots volume and ./test-files off the server. See Upgrades, migrations and backups.
  • Updates. git pull && docker compose up -d --build. Migrations run when the backend starts.
  • Monitoring. LOG_FORMAT=json for log shippers, SENTRY_DSN for errors, and Prometheus on 127.0.0.1:8000/metrics. See Monitoring.
  • Disk. Screenshots accumulate under the screenshots volume. Move them to object storage with a lifecycle rule if disk is tight.