Docker Images
Local development only. Both the Orchestrator and Playwright runner run as Docker images that must be built on your local machine before use.
Why Docker?
The Orchestrator and Playwright runner are isolated in Docker for two reasons:
- Isolation — test runs cannot affect the host system or each other.
- Version control — multiple Playwright versions can coexist as tagged images.
The images are built locally (not pulled from a registry) so changes to the runner code are reflected immediately after a rebuild.
Build-Time Integration Boundary
Marketplace integration code is never installed while a workflow is running.
The Orchestrator build installs the dependencies declared in its lockfile and
scans its installed direct production dependencies for package-owned
playrunner.integration.orchestrator metadata. It validates those declarations,
generates static imports, and bundles the contributions into dist/index.js.
Jira and Slack currently declare this surface; host-managed Environment,
GitHub, Playwright, and Schedule nodes remain part of the Orchestrator host.
Runtime configuration only selects an executor that is already in that image and passes it provider-scoped credentials, node configuration, templates, logging, cancellation, and other host capabilities. Adding, removing, or upgrading an executable integration requires updating the build inputs, rebuilding the Orchestrator image, and replacing or redeploying the running image.
Building the Images
start-local.sh builds all images automatically. You can also build them manually:
Orchestrator
Use the repo helper from the workspace root. It builds with the repository as the Docker context, then removes the existing local Orchestrator container so a new one will be created from the rebuilt image:
./infra/scripts/rebuild-orchestrator.sh
The equivalent image-only command is:
docker build \
--build-arg BASE_PATH=. \
-f apps/runners/orchestrator/Dockerfile \
-t playrunner-orchestrator \
.
Do not use apps/runners/orchestrator as the build context. The image needs the
repository-root context so its local file: package dependencies and static
integration-composition generator are available during the build.
Base image: node:24-alpine
Extra packages installed: docker-cli (so the container can run docker commands against the host socket)
Entry point: node dist/index.js
Playwright Runner
The Playwright runner supports multiple Playwright versions via a shared config file and Docker build argument:
# Inspect configured versions
node infra/scripts/playwright-runner-config.mjs json
# Build one configured version explicitly
docker build \
--platform linux/amd64 \
-f ./apps/runners/playwright/Dockerfile.typescript \
--build-arg PLAYWRIGHT_VERSION=v1.59.0-jammy \
-t playrunner-playwright-runner-typescript:latest \
-t playrunner-playwright-runner-typescript:v1.59.0-jammy \
./apps/runners/playwright
docker build \
--platform linux/amd64 \
-f ./apps/runners/playwright/Dockerfile.python \
--build-arg PLAYWRIGHT_VERSION=v1.59.0-jammy \
-t playrunner-playwright-runner-python:latest \
-t playrunner-playwright-runner-python:v1.59.0-jammy \
./apps/runners/playwright
start-local.sh, infra/gcp/scripts/push-runners.sh, and the Playwright settings modal all read from config/playwright-runner-versions.json.
Base image: mcr.microsoft.com/playwright:${PLAYWRIGHT_VERSION}
Entry point: node dist/index.js
Image Tags
| Tag | Playwright Version | Notes |
|---|---|---|
playrunner-playwright-runner-typescript:latest | Tag with publishAsLatest: true | Alias for the current latest configured TypeScript runner |
playrunner-playwright-runner-typescript:<configured-tag> | Matching configured tag | One TypeScript image per entry in config/playwright-runner-versions.json |
playrunner-playwright-runner-python:latest | Tag with publishAsLatest: true | Alias for the current latest configured Python runner |
playrunner-playwright-runner-python:<configured-tag> | Matching configured tag | One Python image per entry in config/playwright-runner-versions.json |
playrunner-orchestrator | — | The orchestrator image (no version tagging) |
The Playwright node in the editor exposes a playwrightVersion config field and a runtime selector. The Orchestrator uses both values to choose the Docker image tag (for example playrunner-playwright-runner-python:v1.59.0-jammy).
How the Orchestrator Prepares Playwright Containers
When the Orchestrator receives a workflow, it scans the whole graph for Playwright nodes and starts their containers in preparation mode before the DAG reaches those nodes. Locally, each prepared runner starts with the equivalent of:
docker run --rm \
-e GCP_PROJECT=local-dev \
-e PUBSUB_EMULATOR_HOST=host.docker.internal:8084 \
-e MY_ENV_VAR=value \ # user-defined env vars from the Environment node
-e PAYLOAD='{"data":{...},...}' \ # full JSON config
playrunner-playwright-runner-typescript:<configured-tag>
The --rm flag ensures the container is deleted after it exits. No Docker volumes are mounted for the Playwright runner — local output archives are uploaded to the API via HTTP before the container exits, while logs, states, runner_control, runner_status, and output events go through Pub/Sub.
Docker Socket Access
The Orchestrator container receives the host Docker socket mounted as a volume:
-v /var/run/docker.sock:/var/run/docker.sock
This is what allows the Orchestrator (running inside Docker) to spawn Playwright containers on the host's Docker daemon. The containers it spawns appear as siblings, not children, in docker ps.
Rebuilding After Code Changes
If you modify Orchestrator source, an integration's manifest or Orchestrator contribution, the provider-agnostic integration resolver, the selected dependencies, or Playwright runner source, rebuild the affected image before the change can take effect:
# Rebuild the Orchestrator, including all generated package contribution imports,
# and remove the currently running local Orchestrator container
./infra/scripts/rebuild-orchestrator.sh
# Rebuild playwright runners from shared config
./start-local.sh
After the Orchestrator helper finishes, reopen the Editor tab. That triggers
POST /api/runners/start and starts a fresh container from the new image.
Publishing a package or changing marketplace metadata alone does not change a running Orchestrator. The deployment must use an image rebuilt from the updated dependency, lockfile, and package-owned manifest inputs. Package authors never add their provider to a common source registry.
Publishing to GCP
For the end-to-end infrastructure and GCP integration runbook, see GCP Setup. This section documents the image publishing helper itself.
Running workflows against the GCP execution path requires the Orchestrator and Playwright images to be available in a registry that Cloud Run can pull from. The repo ships a helper that builds and pushes them to Google Artifact Registry, redeploys the Orchestrator Cloud Run service, and clears stale Playwright Cloud Run Jobs so they pick up the new image:
./infra/gcp/scripts/push-runners.sh
When only the Orchestrator or its bundled integration executors changed, use:
./infra/gcp/scripts/push-runners.sh --target orchestrator --yes
Prerequisites
- Connect GCP in the Integrations modal. The script reads the GCP project, region, Cloud Run service name, service defaults, and generated image URI templates from the
CloudCredentialrow that the modal writes to Postgres. - Generate Terraform variables and apply Terraform at least once so the required GCP APIs are enabled and the
orchestratorandplaywright-runnerArtifact Registry repositories and shared workflow-events Pub/Sub topic exist in your project. Run./infra/gcp/scripts/setup-terraform.sh, reviewinfra/gcp/terraform.tfvars, then runterraform -chdir=infra/gcp init,terraform -chdir=infra/gcp plan, andterraform -chdir=infra/gcp apply. Repeat this before publishing if the saved project, region, repository path, or workflow-events topic changed. - Local tooling on
PATH:docker,gcloud, andnode.apps/api/.envmust contain a workingDATABASE_URL(the script reuses Prisma fromapps/api/node_modulesto read the credential). - GCloud authentication: run
gcloud auth loginbefore publishing. The push script configures Docker's Artifact Registry credential helper for the target registry host automatically.
Reading the stored settings
If you want to confirm what the script will use before running it:
node infra/gcp/scripts/gcp-settings.mjs json
This only emits the non-secret fields (selectedProject, cloudRunLocation, orchestratorServiceName, orchestratorMinInstanceCount, orchestratorMaxInstanceCount, orchestratorCpuIdle, orchestratorImageUriTemplate, playwrightImageUriTemplate, schedulerServiceAccountEmail). Each value is also available as its own subcommand: project-id, region, orchestrator-service-name, orchestrator-min-instance-count, orchestrator-max-instance-count, orchestrator-cpu-idle, orchestrator-image-uri-template, playwright-image-uri-template, scheduler-service-account-email. Run ./infra/gcp/scripts/push-runners.sh --help for the wrapper script's options.
If these saved values changed, rerun the push script after saving:
./infra/gcp/scripts/push-runners.sh --target both --yes
Run Terraform first when the changed values point at a project, region, Artifact Registry repository, or Pub/Sub topic that does not already exist.
Usage
Interactive (confirmation prompt, then menu for orchestrator / playwright / both):
./infra/gcp/scripts/push-runners.sh
Non-interactive, e.g. for CI:
./infra/gcp/scripts/push-runners.sh --target both --yes
One-off override (push to a project other than the one saved in the modal):
./infra/gcp/scripts/push-runners.sh --project-id my-other-project --target orchestrator --yes
All flags:
| Flag | Effect |
|---|---|
--project-id <id> | Override the stored GCP project ID |
--region <region> | Override the stored Cloud Run region |
--orchestrator-service-name <n> | Override the stored Cloud Run service name |
--orchestrator-min-instances <n> | Override the stored Orchestrator minimum instance count; 0 is allowed |
--orchestrator-max-instances <n> | Override the stored Orchestrator maximum instance count |
--orchestrator-cpu-idle <true|false> | Override the stored Orchestrator CPU idle policy |
--user-id <id> | Filter the Postgres lookup when multiple users have GCP credentials |
--target orchestrator|playwright|both | Skip the interactive menu |
--base-path <path> | BASE_PATH build arg for the Orchestrator image (default .) |
--yes, -y | Skip the confirmation prompt |
What it does
- Docker auth: configures Docker for the target Artifact Registry host with
gcloud auth configure-docker. - Orchestrator: builds the Orchestrator and the package executors selected as
direct production dependencies from the repository context as
linux/amd64. The build generates their static imports, tags the image usingorchestratorImageUriTemplate(with{projectId}substituted), pushes it, and runsgcloud run deploy <orchestratorServiceName> --image ... --min-instances ... --max-instances ... --cpu-boost --[no-]cpu-throttlingto roll out the new image and service settings. - Playwright: for both
typescriptandpython, builds every tag inconfig/playwright-runner-versions.json. The tag flaggedpublishAsLatest: trueis additionally pushed as:latest. Tags useplaywrightImageUriTemplatewith{projectId},{runtime}, and{version}substituted. - Job cleanup: deletes any existing Cloud Run Jobs named
playrunner-*in the configured region so the next workflow execution recreates them with the new image.