From 264da9ef00fa19fa0a645e02403fc515de180ac5 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 3 Nov 2026 11:41:31 -0800 Subject: [PATCH 1/7] fix(docker): build the image from an allowlisted context in a builder stage - Send only the build inputs to the Docker build context: .dockerignore is an allowlist, which keeps VCS metadata and CI-written files out of it. - Bind-mount the build context into a separate builder stage instead of COPYing it. The final stage gets only the playwright-java artifacts of the local Maven repository and runs the CLI from a bind mount of the builder's classes and runtime dependencies. --- .dockerignore | 15 +++++++---- utils/docker/Dockerfile.jammy | 46 +++++++++++++++++++------------- utils/docker/Dockerfile.noble | 46 +++++++++++++++++++------------- utils/docker/Dockerfile.resolute | 46 +++++++++++++++++++------------- utils/docker/build.sh | 4 +-- 5 files changed, 96 insertions(+), 61 deletions(-) diff --git a/.dockerignore b/.dockerignore index 7dc6ab376..dcc73146f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,13 @@ -# The repository root is the Docker build context (see utils/docker/build.sh), -# and the image build copies all of it. Keep VCS metadata, registry credentials -# and local environment files out of the build context. +# The repository root is the Docker build context (see utils/docker/build.sh). +# Send only what the image build reads, which keeps VCS metadata, registry +# credentials and local environment files out of the build context. +* +!pom.xml +!driver +!driver-bundle +!playwright +!utils/docker/maven +**/target **/.git **/.npmrc **/.env* -# Downloaded by publish_docker.sh after the first push. -utils/docker/oras diff --git a/utils/docker/Dockerfile.jammy b/utils/docker/Dockerfile.jammy index 316a95d4f..62b543c9b 100644 --- a/utils/docker/Dockerfile.jammy +++ b/utils/docker/Dockerfile.jammy @@ -1,5 +1,5 @@ ARG ACR_CACHE_PREFIX -FROM ${ACR_CACHE_PREFIX}ubuntu:jammy +FROM ${ACR_CACHE_PREFIX}ubuntu:jammy AS base ARG DEBIAN_FRONTEND=noninteractive ARG TZ=America/Los_Angeles @@ -38,8 +38,26 @@ RUN --mount=type=bind,source=utils/docker/maven,target=/tmp/maven \ ARG PW_TARGET_ARCH ENV JAVA_HOME=/usr/lib/jvm/java-25-openjdk-${PW_TARGET_ARCH} +# === BUILD PLAYWRIGHT JAVA === + +# The image gets only what the final stage explicitly copies or mounts from this stage. +FROM base AS builder + +# The optional Maven settings secret routes dependency resolution through a mirror +# (see .azure-pipelines/publish-docker.yml). The CLI runs from /build: the classes of +# the playwright module and its runtime dependencies. +RUN --mount=type=bind,source=.,target=/tmp/pw-java,rw \ + --mount=type=secret,id=mavensettings,target=/root/.m2/settings.xml,required=false \ + cd /tmp/pw-java && \ + mvn install -D skipTests --no-transfer-progress && \ + mvn dependency:copy-dependencies -D includeScope=runtime -D outputDirectory=/build/lib \ + -f playwright/pom.xml --no-transfer-progress && \ + cp -r playwright/target/classes /build/classes + # === BAKE BROWSERS INTO IMAGE === +FROM base + # Browsers will remain downloaded in `/ms-playwright`. # Note: make sure to set 777 to the registry so that any user can access # registry. @@ -50,25 +68,17 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright # it into /tmp on every launch. See https://github.com/microsoft/playwright-java/issues/1268. ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver -RUN mkdir /ms-playwright && \ - mkdir /tmp/pw-java +# Keep the playwright-java artifacts in the local Maven repository so that Maven projects +# built in the image resolve them without downloading. +COPY --from=builder /root/.m2/repository/com/microsoft/playwright /root/.m2/repository/com/microsoft/playwright -COPY . /tmp/pw-java +RUN mkdir /ms-playwright -# The optional Maven settings secret routes dependency resolution through a mirror -# (see .azure-pipelines/publish-docker.yml). -RUN --mount=type=secret,id=mavensettings,target=/root/.m2/settings.xml,required=false \ - cd /tmp/pw-java && \ - mvn install -D skipTests --no-transfer-progress && \ - mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ - -D exec.args="install-driver" -f playwright/pom.xml --no-transfer-progress && \ - DEBIAN_FRONTEND=noninteractive mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ - -D exec.args="install-deps" -f playwright/pom.xml --no-transfer-progress && \ - mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ - -D exec.args="install" -f playwright/pom.xml --no-transfer-progress && \ - mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ - -D exec.args="mark-docker-image '${DOCKER_IMAGE_NAME_TEMPLATE}'" -f playwright/pom.xml --no-transfer-progress && \ - rm -rf /tmp/pw-java && \ +RUN --mount=type=bind,from=builder,source=/build,target=/tmp/pw-java \ + java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI install-driver && \ + DEBIAN_FRONTEND=noninteractive java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI install-deps && \ + java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI install && \ + java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ # Workaround for https://github.com/microsoft/playwright/issues/27313 # While the gstreamer plugin load process can be in-process, it ended up throwing # an error that it can't have libsoup2 and libsoup3 in the same process because diff --git a/utils/docker/Dockerfile.noble b/utils/docker/Dockerfile.noble index 5ccd4d7bf..79dca687c 100644 --- a/utils/docker/Dockerfile.noble +++ b/utils/docker/Dockerfile.noble @@ -1,5 +1,5 @@ ARG ACR_CACHE_PREFIX -FROM ${ACR_CACHE_PREFIX}ubuntu:noble +FROM ${ACR_CACHE_PREFIX}ubuntu:noble AS base ARG DEBIAN_FRONTEND=noninteractive ARG TZ=America/Los_Angeles @@ -38,8 +38,26 @@ RUN --mount=type=bind,source=utils/docker/maven,target=/tmp/maven \ ARG PW_TARGET_ARCH ENV JAVA_HOME=/usr/lib/jvm/java-25-openjdk-${PW_TARGET_ARCH} +# === BUILD PLAYWRIGHT JAVA === + +# The image gets only what the final stage explicitly copies or mounts from this stage. +FROM base AS builder + +# The optional Maven settings secret routes dependency resolution through a mirror +# (see .azure-pipelines/publish-docker.yml). The CLI runs from /build: the classes of +# the playwright module and its runtime dependencies. +RUN --mount=type=bind,source=.,target=/tmp/pw-java,rw \ + --mount=type=secret,id=mavensettings,target=/root/.m2/settings.xml,required=false \ + cd /tmp/pw-java && \ + mvn install -D skipTests --no-transfer-progress && \ + mvn dependency:copy-dependencies -D includeScope=runtime -D outputDirectory=/build/lib \ + -f playwright/pom.xml --no-transfer-progress && \ + cp -r playwright/target/classes /build/classes + # === BAKE BROWSERS INTO IMAGE === +FROM base + # Browsers will remain downloaded in `/ms-playwright`. # Note: make sure to set 777 to the registry so that any user can access # registry. @@ -50,24 +68,16 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright # it into /tmp on every launch. See https://github.com/microsoft/playwright-java/issues/1268. ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver -RUN mkdir /ms-playwright && \ - mkdir /tmp/pw-java +# Keep the playwright-java artifacts in the local Maven repository so that Maven projects +# built in the image resolve them without downloading. +COPY --from=builder /root/.m2/repository/com/microsoft/playwright /root/.m2/repository/com/microsoft/playwright -COPY . /tmp/pw-java +RUN mkdir /ms-playwright -# The optional Maven settings secret routes dependency resolution through a mirror -# (see .azure-pipelines/publish-docker.yml). -RUN --mount=type=secret,id=mavensettings,target=/root/.m2/settings.xml,required=false \ - cd /tmp/pw-java && \ - mvn install -D skipTests --no-transfer-progress && \ - mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ - -D exec.args="install-driver" -f playwright/pom.xml --no-transfer-progress && \ - DEBIAN_FRONTEND=noninteractive mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ - -D exec.args="install-deps" -f playwright/pom.xml --no-transfer-progress && \ - mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ - -D exec.args="install" -f playwright/pom.xml --no-transfer-progress && \ - mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ - -D exec.args="mark-docker-image '${DOCKER_IMAGE_NAME_TEMPLATE}'" -f playwright/pom.xml --no-transfer-progress && \ - rm -rf /tmp/pw-java && \ +RUN --mount=type=bind,from=builder,source=/build,target=/tmp/pw-java \ + java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI install-driver && \ + DEBIAN_FRONTEND=noninteractive java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI install-deps && \ + java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI install && \ + java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH && \ chmod -R 777 $PLAYWRIGHT_DRIVER_DIR diff --git a/utils/docker/Dockerfile.resolute b/utils/docker/Dockerfile.resolute index 7dabf1f96..b350cd0f5 100644 --- a/utils/docker/Dockerfile.resolute +++ b/utils/docker/Dockerfile.resolute @@ -1,5 +1,5 @@ ARG ACR_CACHE_PREFIX -FROM ${ACR_CACHE_PREFIX}ubuntu:resolute +FROM ${ACR_CACHE_PREFIX}ubuntu:resolute AS base ARG DEBIAN_FRONTEND=noninteractive ARG TZ=America/Los_Angeles @@ -32,8 +32,26 @@ RUN find /etc/apt -type f \( -name '*.list' -o -name '*.sources' \) \ ARG PW_TARGET_ARCH ENV JAVA_HOME=/usr/lib/jvm/java-25-openjdk-${PW_TARGET_ARCH} +# === BUILD PLAYWRIGHT JAVA === + +# The image gets only what the final stage explicitly copies or mounts from this stage. +FROM base AS builder + +# The optional Maven settings secret routes dependency resolution through a mirror +# (see .azure-pipelines/publish-docker.yml). The CLI runs from /build: the classes of +# the playwright module and its runtime dependencies. +RUN --mount=type=bind,source=.,target=/tmp/pw-java,rw \ + --mount=type=secret,id=mavensettings,target=/root/.m2/settings.xml,required=false \ + cd /tmp/pw-java && \ + mvn install -D skipTests --no-transfer-progress && \ + mvn dependency:copy-dependencies -D includeScope=runtime -D outputDirectory=/build/lib \ + -f playwright/pom.xml --no-transfer-progress && \ + cp -r playwright/target/classes /build/classes + # === BAKE BROWSERS INTO IMAGE === +FROM base + # Browsers will remain downloaded in `/ms-playwright`. # Note: make sure to set 777 to the registry so that any user can access # registry. @@ -44,24 +62,16 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright # it into /tmp on every launch. See https://github.com/microsoft/playwright-java/issues/1268. ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver -RUN mkdir /ms-playwright && \ - mkdir /tmp/pw-java +# Keep the playwright-java artifacts in the local Maven repository so that Maven projects +# built in the image resolve them without downloading. +COPY --from=builder /root/.m2/repository/com/microsoft/playwright /root/.m2/repository/com/microsoft/playwright -COPY . /tmp/pw-java +RUN mkdir /ms-playwright -# The optional Maven settings secret routes dependency resolution through a mirror -# (see .azure-pipelines/publish-docker.yml). -RUN --mount=type=secret,id=mavensettings,target=/root/.m2/settings.xml,required=false \ - cd /tmp/pw-java && \ - mvn install -D skipTests --no-transfer-progress && \ - mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ - -D exec.args="install-driver" -f playwright/pom.xml --no-transfer-progress && \ - DEBIAN_FRONTEND=noninteractive mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ - -D exec.args="install-deps" -f playwright/pom.xml --no-transfer-progress && \ - mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ - -D exec.args="install" -f playwright/pom.xml --no-transfer-progress && \ - mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ - -D exec.args="mark-docker-image '${DOCKER_IMAGE_NAME_TEMPLATE}'" -f playwright/pom.xml --no-transfer-progress && \ - rm -rf /tmp/pw-java && \ +RUN --mount=type=bind,from=builder,source=/build,target=/tmp/pw-java \ + java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI install-driver && \ + DEBIAN_FRONTEND=noninteractive java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI install-deps && \ + java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI install && \ + java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH && \ chmod -R 777 $PLAYWRIGHT_DRIVER_DIR diff --git a/utils/docker/build.sh b/utils/docker/build.sh index c1225245d..789e6c088 100755 --- a/utils/docker/build.sh +++ b/utils/docker/build.sh @@ -34,8 +34,8 @@ fi PW_TARGET_ARCH=$(echo $1 | cut -c3-) -# Assemble the driver on the host where npm is available; the Dockerfile picks -# it up via `COPY . /tmp/pw-java`. +# Assemble the driver on the host where npm is available; the builder stage of the +# Dockerfile picks it up from the build context. ../../scripts/download_driver.sh # The jammy and noble images install this Maven (see the Dockerfiles). Fetch it with the From 0ea6f52e1ec1ceec6cfc883c402f08ec22cf39ab Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 3 Nov 2026 15:22:49 -0800 Subject: [PATCH 2/7] fix(docker): declare the image name template in the stage that uses it --- utils/docker/Dockerfile.jammy | 3 ++- utils/docker/Dockerfile.noble | 3 ++- utils/docker/Dockerfile.resolute | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/utils/docker/Dockerfile.jammy b/utils/docker/Dockerfile.jammy index 62b543c9b..356bb766a 100644 --- a/utils/docker/Dockerfile.jammy +++ b/utils/docker/Dockerfile.jammy @@ -3,7 +3,6 @@ FROM ${ACR_CACHE_PREFIX}ubuntu:jammy AS base ARG DEBIAN_FRONTEND=noninteractive ARG TZ=America/Los_Angeles -ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/java:v%version%-jammy" ARG UBUNTU_MIRROR_PREFIX ENV LANG=C.UTF-8 @@ -58,6 +57,8 @@ RUN --mount=type=bind,source=.,target=/tmp/pw-java,rw \ FROM base +ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/java:v%version%-jammy" + # Browsers will remain downloaded in `/ms-playwright`. # Note: make sure to set 777 to the registry so that any user can access # registry. diff --git a/utils/docker/Dockerfile.noble b/utils/docker/Dockerfile.noble index 79dca687c..b9a7a72df 100644 --- a/utils/docker/Dockerfile.noble +++ b/utils/docker/Dockerfile.noble @@ -3,7 +3,6 @@ FROM ${ACR_CACHE_PREFIX}ubuntu:noble AS base ARG DEBIAN_FRONTEND=noninteractive ARG TZ=America/Los_Angeles -ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/java:v%version%-noble" ARG UBUNTU_MIRROR_PREFIX ENV LANG=C.UTF-8 @@ -58,6 +57,8 @@ RUN --mount=type=bind,source=.,target=/tmp/pw-java,rw \ FROM base +ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/java:v%version%-noble" + # Browsers will remain downloaded in `/ms-playwright`. # Note: make sure to set 777 to the registry so that any user can access # registry. diff --git a/utils/docker/Dockerfile.resolute b/utils/docker/Dockerfile.resolute index b350cd0f5..947a2e4af 100644 --- a/utils/docker/Dockerfile.resolute +++ b/utils/docker/Dockerfile.resolute @@ -3,7 +3,6 @@ FROM ${ACR_CACHE_PREFIX}ubuntu:resolute AS base ARG DEBIAN_FRONTEND=noninteractive ARG TZ=America/Los_Angeles -ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/java:v%version%-resolute" ARG UBUNTU_MIRROR_PREFIX ENV LANG=C.UTF-8 @@ -52,6 +51,8 @@ RUN --mount=type=bind,source=.,target=/tmp/pw-java,rw \ FROM base +ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/java:v%version%-resolute" + # Browsers will remain downloaded in `/ms-playwright`. # Note: make sure to set 777 to the registry so that any user can access # registry. From d50ce73a004c8997cf5b085c8253ce50272dd71f Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 3 Nov 2026 16:46:02 -0800 Subject: [PATCH 3/7] fix(docker): build the client on the host and make utils/docker the build context Like the other language ports, build.sh builds the client on the host and stages what the image needs under utils/docker/dist: the playwright-java artifacts for the local Maven repository and the CLI jars. The Dockerfiles COPY the former and run the CLI from a bind mount of the latter, so the image no longer builds from source and the repository is not part of the build context. The root .dockerignore is no longer needed. --- .dockerignore | 13 -------- .gitignore | 3 ++ utils/docker/Dockerfile.jammy | 42 ++++++++------------------ utils/docker/Dockerfile.noble | 42 ++++++++------------------ utils/docker/Dockerfile.resolute | 40 +++++++------------------ utils/docker/build.sh | 51 ++++++++++++++++++++++---------- 6 files changed, 73 insertions(+), 118 deletions(-) delete mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index dcc73146f..000000000 --- a/.dockerignore +++ /dev/null @@ -1,13 +0,0 @@ -# The repository root is the Docker build context (see utils/docker/build.sh). -# Send only what the image build reads, which keeps VCS metadata, registry -# credentials and local environment files out of the build context. -* -!pom.xml -!driver -!driver-bundle -!playwright -!utils/docker/maven -**/target -**/.git -**/.npmrc -**/.env* diff --git a/.gitignore b/.gitignore index 33685ce73..892a88090 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ target/ *.iml .npmrc + +# Staged by utils/docker/build.sh for the Docker build context +utils/docker/dist/ diff --git a/utils/docker/Dockerfile.jammy b/utils/docker/Dockerfile.jammy index 356bb766a..441e1adc4 100644 --- a/utils/docker/Dockerfile.jammy +++ b/utils/docker/Dockerfile.jammy @@ -1,8 +1,9 @@ ARG ACR_CACHE_PREFIX -FROM ${ACR_CACHE_PREFIX}ubuntu:jammy AS base +FROM ${ACR_CACHE_PREFIX}ubuntu:jammy ARG DEBIAN_FRONTEND=noninteractive ARG TZ=America/Los_Angeles +ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/java:v%version%-jammy" ARG UBUNTU_MIRROR_PREFIX ENV LANG=C.UTF-8 @@ -30,35 +31,15 @@ RUN find /etc/apt -type f \( -name '*.list' -o -name '*.sources' \) \ # Java 25, so we install latest Maven from Apache instead. build.sh # downloads it into utils/docker/maven. ARG MAVEN_VERSION -RUN --mount=type=bind,source=utils/docker/maven,target=/tmp/maven \ +RUN --mount=type=bind,source=maven,target=/tmp/maven \ tar zxf /tmp/maven/apache-maven-$MAVEN_VERSION-bin.tar.gz -C /opt/ && \ ln -s /opt/apache-maven-$MAVEN_VERSION/bin/mvn /usr/local/bin/ ARG PW_TARGET_ARCH ENV JAVA_HOME=/usr/lib/jvm/java-25-openjdk-${PW_TARGET_ARCH} -# === BUILD PLAYWRIGHT JAVA === - -# The image gets only what the final stage explicitly copies or mounts from this stage. -FROM base AS builder - -# The optional Maven settings secret routes dependency resolution through a mirror -# (see .azure-pipelines/publish-docker.yml). The CLI runs from /build: the classes of -# the playwright module and its runtime dependencies. -RUN --mount=type=bind,source=.,target=/tmp/pw-java,rw \ - --mount=type=secret,id=mavensettings,target=/root/.m2/settings.xml,required=false \ - cd /tmp/pw-java && \ - mvn install -D skipTests --no-transfer-progress && \ - mvn dependency:copy-dependencies -D includeScope=runtime -D outputDirectory=/build/lib \ - -f playwright/pom.xml --no-transfer-progress && \ - cp -r playwright/target/classes /build/classes - # === BAKE BROWSERS INTO IMAGE === -FROM base - -ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/java:v%version%-jammy" - # Browsers will remain downloaded in `/ms-playwright`. # Note: make sure to set 777 to the registry so that any user can access # registry. @@ -69,17 +50,18 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright # it into /tmp on every launch. See https://github.com/microsoft/playwright-java/issues/1268. ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver -# Keep the playwright-java artifacts in the local Maven repository so that Maven projects -# built in the image resolve them without downloading. -COPY --from=builder /root/.m2/repository/com/microsoft/playwright /root/.m2/repository/com/microsoft/playwright +# The playwright-java artifacts built by build.sh go into the local Maven repository so +# that Maven projects built in the image resolve them without downloading. +COPY ./dist/m2 /root/.m2/repository RUN mkdir /ms-playwright -RUN --mount=type=bind,from=builder,source=/build,target=/tmp/pw-java \ - java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI install-driver && \ - DEBIAN_FRONTEND=noninteractive java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI install-deps && \ - java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI install && \ - java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ +# The CLI runs from the jars staged by build.sh; the mount leaves nothing in the image. +RUN --mount=type=bind,source=dist/cli,target=/tmp/pw-java \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install-driver && \ + DEBIAN_FRONTEND=noninteractive java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install-deps && \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install && \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ # Workaround for https://github.com/microsoft/playwright/issues/27313 # While the gstreamer plugin load process can be in-process, it ended up throwing # an error that it can't have libsoup2 and libsoup3 in the same process because diff --git a/utils/docker/Dockerfile.noble b/utils/docker/Dockerfile.noble index b9a7a72df..15a017bb1 100644 --- a/utils/docker/Dockerfile.noble +++ b/utils/docker/Dockerfile.noble @@ -1,8 +1,9 @@ ARG ACR_CACHE_PREFIX -FROM ${ACR_CACHE_PREFIX}ubuntu:noble AS base +FROM ${ACR_CACHE_PREFIX}ubuntu:noble ARG DEBIAN_FRONTEND=noninteractive ARG TZ=America/Los_Angeles +ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/java:v%version%-noble" ARG UBUNTU_MIRROR_PREFIX ENV LANG=C.UTF-8 @@ -30,35 +31,15 @@ RUN find /etc/apt -type f \( -name '*.list' -o -name '*.sources' \) \ # Java 25, so we install latest Maven from Apache instead. build.sh # downloads it into utils/docker/maven. ARG MAVEN_VERSION -RUN --mount=type=bind,source=utils/docker/maven,target=/tmp/maven \ +RUN --mount=type=bind,source=maven,target=/tmp/maven \ tar zxf /tmp/maven/apache-maven-$MAVEN_VERSION-bin.tar.gz -C /opt/ && \ ln -s /opt/apache-maven-$MAVEN_VERSION/bin/mvn /usr/local/bin/ ARG PW_TARGET_ARCH ENV JAVA_HOME=/usr/lib/jvm/java-25-openjdk-${PW_TARGET_ARCH} -# === BUILD PLAYWRIGHT JAVA === - -# The image gets only what the final stage explicitly copies or mounts from this stage. -FROM base AS builder - -# The optional Maven settings secret routes dependency resolution through a mirror -# (see .azure-pipelines/publish-docker.yml). The CLI runs from /build: the classes of -# the playwright module and its runtime dependencies. -RUN --mount=type=bind,source=.,target=/tmp/pw-java,rw \ - --mount=type=secret,id=mavensettings,target=/root/.m2/settings.xml,required=false \ - cd /tmp/pw-java && \ - mvn install -D skipTests --no-transfer-progress && \ - mvn dependency:copy-dependencies -D includeScope=runtime -D outputDirectory=/build/lib \ - -f playwright/pom.xml --no-transfer-progress && \ - cp -r playwright/target/classes /build/classes - # === BAKE BROWSERS INTO IMAGE === -FROM base - -ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/java:v%version%-noble" - # Browsers will remain downloaded in `/ms-playwright`. # Note: make sure to set 777 to the registry so that any user can access # registry. @@ -69,16 +50,17 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright # it into /tmp on every launch. See https://github.com/microsoft/playwright-java/issues/1268. ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver -# Keep the playwright-java artifacts in the local Maven repository so that Maven projects -# built in the image resolve them without downloading. -COPY --from=builder /root/.m2/repository/com/microsoft/playwright /root/.m2/repository/com/microsoft/playwright +# The playwright-java artifacts built by build.sh go into the local Maven repository so +# that Maven projects built in the image resolve them without downloading. +COPY ./dist/m2 /root/.m2/repository RUN mkdir /ms-playwright -RUN --mount=type=bind,from=builder,source=/build,target=/tmp/pw-java \ - java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI install-driver && \ - DEBIAN_FRONTEND=noninteractive java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI install-deps && \ - java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI install && \ - java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ +# The CLI runs from the jars staged by build.sh; the mount leaves nothing in the image. +RUN --mount=type=bind,source=dist/cli,target=/tmp/pw-java \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install-driver && \ + DEBIAN_FRONTEND=noninteractive java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install-deps && \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install && \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH && \ chmod -R 777 $PLAYWRIGHT_DRIVER_DIR diff --git a/utils/docker/Dockerfile.resolute b/utils/docker/Dockerfile.resolute index 947a2e4af..641e61d6e 100644 --- a/utils/docker/Dockerfile.resolute +++ b/utils/docker/Dockerfile.resolute @@ -1,8 +1,9 @@ ARG ACR_CACHE_PREFIX -FROM ${ACR_CACHE_PREFIX}ubuntu:resolute AS base +FROM ${ACR_CACHE_PREFIX}ubuntu:resolute ARG DEBIAN_FRONTEND=noninteractive ARG TZ=America/Los_Angeles +ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/java:v%version%-resolute" ARG UBUNTU_MIRROR_PREFIX ENV LANG=C.UTF-8 @@ -31,28 +32,8 @@ RUN find /etc/apt -type f \( -name '*.list' -o -name '*.sources' \) \ ARG PW_TARGET_ARCH ENV JAVA_HOME=/usr/lib/jvm/java-25-openjdk-${PW_TARGET_ARCH} -# === BUILD PLAYWRIGHT JAVA === - -# The image gets only what the final stage explicitly copies or mounts from this stage. -FROM base AS builder - -# The optional Maven settings secret routes dependency resolution through a mirror -# (see .azure-pipelines/publish-docker.yml). The CLI runs from /build: the classes of -# the playwright module and its runtime dependencies. -RUN --mount=type=bind,source=.,target=/tmp/pw-java,rw \ - --mount=type=secret,id=mavensettings,target=/root/.m2/settings.xml,required=false \ - cd /tmp/pw-java && \ - mvn install -D skipTests --no-transfer-progress && \ - mvn dependency:copy-dependencies -D includeScope=runtime -D outputDirectory=/build/lib \ - -f playwright/pom.xml --no-transfer-progress && \ - cp -r playwright/target/classes /build/classes - # === BAKE BROWSERS INTO IMAGE === -FROM base - -ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/java:v%version%-resolute" - # Browsers will remain downloaded in `/ms-playwright`. # Note: make sure to set 777 to the registry so that any user can access # registry. @@ -63,16 +44,17 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright # it into /tmp on every launch. See https://github.com/microsoft/playwright-java/issues/1268. ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver -# Keep the playwright-java artifacts in the local Maven repository so that Maven projects -# built in the image resolve them without downloading. -COPY --from=builder /root/.m2/repository/com/microsoft/playwright /root/.m2/repository/com/microsoft/playwright +# The playwright-java artifacts built by build.sh go into the local Maven repository so +# that Maven projects built in the image resolve them without downloading. +COPY ./dist/m2 /root/.m2/repository RUN mkdir /ms-playwright -RUN --mount=type=bind,from=builder,source=/build,target=/tmp/pw-java \ - java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI install-driver && \ - DEBIAN_FRONTEND=noninteractive java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI install-deps && \ - java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI install && \ - java -cp '/tmp/pw-java/classes:/tmp/pw-java/lib/*' com.microsoft.playwright.CLI mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ +# The CLI runs from the jars staged by build.sh; the mount leaves nothing in the image. +RUN --mount=type=bind,source=dist/cli,target=/tmp/pw-java \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install-driver && \ + DEBIAN_FRONTEND=noninteractive java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install-deps && \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install && \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH && \ chmod -R 777 $PLAYWRIGHT_DRIVER_DIR diff --git a/utils/docker/build.sh b/utils/docker/build.sh index 789e6c088..282828060 100755 --- a/utils/docker/build.sh +++ b/utils/docker/build.sh @@ -10,13 +10,14 @@ if [[ ($1 == '--help') || ($1 == '-h') || ($1 == '') || ($2 == '') ]]; then echo "" echo " docker run --rm -it playwright:localbuild-noble /bin/bash" echo "" - echo "NOTE: this requires on Playwright PIP dependencies to be installed" + echo "NOTE: this requires a JDK, Maven and npm on the host: the client is built here" + echo "and the image only installs the result." echo "" exit 0 fi function cleanup() { - : + rm -rf dist } trap "cleanup; cd $(pwd -P)" EXIT @@ -34,21 +35,40 @@ fi PW_TARGET_ARCH=$(echo $1 | cut -c3-) -# Assemble the driver on the host where npm is available; the builder stage of the -# Dockerfile picks it up from the build context. +# The release pipeline cannot reach Maven Central directly: it provides a settings.xml +# that mirrors it to an internal feed (see .azure-pipelines/publish-docker.yml). +MVN=(mvn --no-transfer-progress) +if [[ -n "${MAVEN_SETTINGS_SECRET:-}" ]]; then + MVN+=(-s "${MAVEN_SETTINGS_SECRET}") +fi + +# Like the other language ports, build the client on the host, where npm, a JDK and +# Maven are available, and stage what the Dockerfile copies into the image under dist/. +# The build context is this directory, so nothing else reaches the image. ../../scripts/download_driver.sh +"${MVN[@]}" -B install -D skipTests -f ../../pom.xml -# The jammy and noble images install this Maven (see the Dockerfiles). Fetch it with the -# host's Maven so that it resolves through the configured mirror: the release pipeline -# cannot reach Maven Central directly (see .azure-pipelines/settings.xml). -MAVEN_VERSION=3.9.12 -mvn dependency:copy -Dartifact="org.apache.maven:apache-maven:${MAVEN_VERSION}:tar.gz:bin" \ - -DoutputDirectory=maven --no-transfer-progress -q +PW_VERSION=$("${MVN[@]}" -q -f ../../pom.xml help:evaluate -D expression=project.version -D forceStdout) +LOCAL_REPO=$("${MVN[@]}" -q -f ../../pom.xml help:evaluate -D expression=settings.localRepository -D forceStdout) +rm -rf dist -SECRET_ARGS=() -if [[ -n "${MAVEN_SETTINGS_SECRET:-}" ]]; then - SECRET_ARGS+=(--secret "id=mavensettings,src=${MAVEN_SETTINGS_SECRET}") -fi +# dist/m2: the playwright-java artifacts of this build, for the local Maven repository of +# the image. Projects built in the image then resolve them without downloading. +for MODULE in parent-pom driver driver-bundle playwright; do + mkdir -p "dist/m2/com/microsoft/playwright/$MODULE" + cp -R "$LOCAL_REPO/com/microsoft/playwright/$MODULE/$PW_VERSION" "dist/m2/com/microsoft/playwright/$MODULE/" +done + +# dist/cli: the client jar and its runtime dependencies, which the Dockerfile runs the +# CLI from to install the driver, the browsers and their dependencies. +"${MVN[@]}" -q -f ../../playwright/pom.xml dependency:copy-dependencies \ + -D includeScope=runtime -D outputDirectory="$PWD/dist/cli" +cp "../../playwright/target/playwright-$PW_VERSION.jar" dist/cli/ + +# The jammy and noble images install this Maven (see the Dockerfiles). +MAVEN_VERSION=3.9.12 +"${MVN[@]}" -q dependency:copy -D artifact="org.apache.maven:apache-maven:${MAVEN_VERSION}:tar.gz:bin" \ + -D outputDirectory=maven # Keep each arch image a plain single-platform manifest without the unknown/unknown platform entry. export BUILDX_NO_DEFAULT_ATTESTATIONS=1 @@ -69,8 +89,7 @@ for ((attempt = 1; attempt <= MAX_ATTEMPTS; attempt++)); do --build-arg "MAVEN_VERSION=${MAVEN_VERSION}" \ --build-arg ACR_CACHE_PREFIX="${ACR_CACHE_PREFIX}" \ --build-arg UBUNTU_MIRROR_PREFIX="${UBUNTU_MIRROR_PREFIX}" \ - "${SECRET_ARGS[@]}" \ - -t "$3" -f "Dockerfile.$2" ../../; then + -t "$3" -f "Dockerfile.$2" .; then exit 0 fi if (( attempt < MAX_ATTEMPTS )); then From 2cf9ac818af5860cf9fc4a52596db286f310e972 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 3 Nov 2026 17:04:11 -0800 Subject: [PATCH 4/7] devops(docker): describe the host build in the publish pipeline and ignore the rest of the context Reword the pipeline comments for the client build on the host, and add a .dockerignore for the utils/docker build context that admits only dist/ and maven/. --- .azure-pipelines/publish-docker.yml | 8 ++++---- utils/docker/.dockerignore | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 utils/docker/.dockerignore diff --git a/.azure-pipelines/publish-docker.yml b/.azure-pipelines/publish-docker.yml index 53095d1b4..86d941d08 100644 --- a/.azure-pipelines/publish-docker.yml +++ b/.azure-pipelines/publish-docker.yml @@ -69,8 +69,8 @@ extends: echo '{ "data-root": "/mnt/docker" }' | sudo tee /etc/docker/daemon.json sudo service docker start - # Used by scripts/download_driver.sh on the host. The repository root is the - # Docker build context, so .dockerignore keeps this file out of the images. + # Used by scripts/download_driver.sh on the host. The Docker build context is + # utils/docker (see build.sh), so this file never reaches the images. - task: Bash@3 displayName: "setup .npmrc" inputs: @@ -115,8 +115,8 @@ extends: displayName: "Build & publish Docker images" inputs: targetType: "inline" - # The authenticated settings.xml is passed to `docker build` as a secret so - # that Maven inside the image also resolves from DevDiv_PublicPackages. + # build.sh builds the client on the host with the authenticated settings.xml so + # that it also resolves from DevDiv_PublicPackages. script: MAVEN_SETTINGS_SECRET="$HOME/.m2/settings.xml" ./utils/docker/publish_docker.sh ${{ parameters.releaseChannel }} env: ACR_CACHE_PREFIX: "playwright.azurecr.io/cached/" diff --git a/utils/docker/.dockerignore b/utils/docker/.dockerignore new file mode 100644 index 000000000..9a150a830 --- /dev/null +++ b/utils/docker/.dockerignore @@ -0,0 +1,6 @@ +# This directory is the Docker build context (see build.sh). The image reads only the +# artifacts staged in dist/ and the Maven tarball in maven/; keep everything else, such +# as the oras download of publish_docker.sh, out of the build context. +* +!dist +!maven From cff8151818c20eff4818ef8670a2cc0438d5cadf Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 3 Nov 2026 17:13:37 -0800 Subject: [PATCH 5/7] devops(docker): keep the explicit VCS and credential exclusions in .dockerignore --- utils/docker/.dockerignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/utils/docker/.dockerignore b/utils/docker/.dockerignore index 9a150a830..94c85dfae 100644 --- a/utils/docker/.dockerignore +++ b/utils/docker/.dockerignore @@ -4,3 +4,8 @@ * !dist !maven +# Should the build context ever grow, keep VCS metadata, registry credentials and local +# environment files out of it regardless. +**/.git +**/.npmrc +**/.env* From 33af3e4007e5df5d72eb6f04ee04ec0ca018188d Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 3 Nov 2026 17:28:16 -0800 Subject: [PATCH 6/7] chore(docker): simplify the host build script and the Dockerfiles - Drop MAVEN_SETTINGS_SECRET: the host build reads ~/.m2/settings.xml, which is where the pipeline puts the authenticated settings anyway. - Read the project version and the local repository in one exec:exec call, the plugin pom.xml pins, like publish_docker.sh does. - Fold mkdir /ms-playwright into the CLI step, run install --with-deps instead of install-deps and install, and drop the redundant DEBIAN_FRONTEND prefix. - Ignore utils/docker/dist in utils/docker/.gitignore next to oras. --- .azure-pipelines/publish-docker.yml | 6 +++--- .gitignore | 3 --- utils/docker/.gitignore | 2 +- utils/docker/Dockerfile.jammy | 8 +++----- utils/docker/Dockerfile.noble | 8 +++----- utils/docker/Dockerfile.resolute | 8 +++----- utils/docker/build.sh | 16 +++++++++------- 7 files changed, 22 insertions(+), 29 deletions(-) diff --git a/.azure-pipelines/publish-docker.yml b/.azure-pipelines/publish-docker.yml index 86d941d08..875a2e103 100644 --- a/.azure-pipelines/publish-docker.yml +++ b/.azure-pipelines/publish-docker.yml @@ -115,9 +115,9 @@ extends: displayName: "Build & publish Docker images" inputs: targetType: "inline" - # build.sh builds the client on the host with the authenticated settings.xml so - # that it also resolves from DevDiv_PublicPackages. - script: MAVEN_SETTINGS_SECRET="$HOME/.m2/settings.xml" ./utils/docker/publish_docker.sh ${{ parameters.releaseChannel }} + # build.sh builds the client on the host, where ~/.m2/settings.xml mirrors Maven + # Central to DevDiv_PublicPackages. + script: ./utils/docker/publish_docker.sh ${{ parameters.releaseChannel }} env: ACR_CACHE_PREFIX: "playwright.azurecr.io/cached/" UBUNTU_MIRROR_PREFIX: "azure." diff --git a/.gitignore b/.gitignore index 892a88090..33685ce73 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,3 @@ target/ *.iml .npmrc - -# Staged by utils/docker/build.sh for the Docker build context -utils/docker/dist/ diff --git a/utils/docker/.gitignore b/utils/docker/.gitignore index f5ca1e18c..ba5e6d750 100644 --- a/utils/docker/.gitignore +++ b/utils/docker/.gitignore @@ -1,2 +1,2 @@ oras/ - +dist/ diff --git a/utils/docker/Dockerfile.jammy b/utils/docker/Dockerfile.jammy index 441e1adc4..ea8085b43 100644 --- a/utils/docker/Dockerfile.jammy +++ b/utils/docker/Dockerfile.jammy @@ -52,15 +52,13 @@ ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver # The playwright-java artifacts built by build.sh go into the local Maven repository so # that Maven projects built in the image resolve them without downloading. -COPY ./dist/m2 /root/.m2/repository - -RUN mkdir /ms-playwright +COPY dist/m2 /root/.m2/repository # The CLI runs from the jars staged by build.sh; the mount leaves nothing in the image. RUN --mount=type=bind,source=dist/cli,target=/tmp/pw-java \ + mkdir /ms-playwright && \ java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install-driver && \ - DEBIAN_FRONTEND=noninteractive java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install-deps && \ - java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install && \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install --with-deps && \ java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ # Workaround for https://github.com/microsoft/playwright/issues/27313 # While the gstreamer plugin load process can be in-process, it ended up throwing diff --git a/utils/docker/Dockerfile.noble b/utils/docker/Dockerfile.noble index 15a017bb1..4b2c480fe 100644 --- a/utils/docker/Dockerfile.noble +++ b/utils/docker/Dockerfile.noble @@ -52,15 +52,13 @@ ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver # The playwright-java artifacts built by build.sh go into the local Maven repository so # that Maven projects built in the image resolve them without downloading. -COPY ./dist/m2 /root/.m2/repository - -RUN mkdir /ms-playwright +COPY dist/m2 /root/.m2/repository # The CLI runs from the jars staged by build.sh; the mount leaves nothing in the image. RUN --mount=type=bind,source=dist/cli,target=/tmp/pw-java \ + mkdir /ms-playwright && \ java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install-driver && \ - DEBIAN_FRONTEND=noninteractive java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install-deps && \ - java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install && \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install --with-deps && \ java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH && \ chmod -R 777 $PLAYWRIGHT_DRIVER_DIR diff --git a/utils/docker/Dockerfile.resolute b/utils/docker/Dockerfile.resolute index 641e61d6e..e89578635 100644 --- a/utils/docker/Dockerfile.resolute +++ b/utils/docker/Dockerfile.resolute @@ -46,15 +46,13 @@ ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver # The playwright-java artifacts built by build.sh go into the local Maven repository so # that Maven projects built in the image resolve them without downloading. -COPY ./dist/m2 /root/.m2/repository - -RUN mkdir /ms-playwright +COPY dist/m2 /root/.m2/repository # The CLI runs from the jars staged by build.sh; the mount leaves nothing in the image. RUN --mount=type=bind,source=dist/cli,target=/tmp/pw-java \ + mkdir /ms-playwright && \ java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install-driver && \ - DEBIAN_FRONTEND=noninteractive java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install-deps && \ - java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install && \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install --with-deps && \ java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH && \ chmod -R 777 $PLAYWRIGHT_DRIVER_DIR diff --git a/utils/docker/build.sh b/utils/docker/build.sh index 282828060..c951d9354 100755 --- a/utils/docker/build.sh +++ b/utils/docker/build.sh @@ -35,12 +35,9 @@ fi PW_TARGET_ARCH=$(echo $1 | cut -c3-) -# The release pipeline cannot reach Maven Central directly: it provides a settings.xml -# that mirrors it to an internal feed (see .azure-pipelines/publish-docker.yml). +# The host Maven settings (~/.m2/settings.xml) route dependency resolution through the +# mirror of the release pipeline, which cannot reach Maven Central directly. MVN=(mvn --no-transfer-progress) -if [[ -n "${MAVEN_SETTINGS_SECRET:-}" ]]; then - MVN+=(-s "${MAVEN_SETTINGS_SECRET}") -fi # Like the other language ports, build the client on the host, where npm, a JDK and # Maven are available, and stage what the Dockerfile copies into the image under dist/. @@ -48,8 +45,13 @@ fi ../../scripts/download_driver.sh "${MVN[@]}" -B install -D skipTests -f ../../pom.xml -PW_VERSION=$("${MVN[@]}" -q -f ../../pom.xml help:evaluate -D expression=project.version -D forceStdout) -LOCAL_REPO=$("${MVN[@]}" -q -f ../../pom.xml help:evaluate -D expression=settings.localRepository -D forceStdout) +if ! PW_INFO=$("${MVN[@]}" -q -N -f ../../pom.xml exec:exec -D exec.executable=echo \ + -D exec.args='${project.version} ${settings.localRepository}'); then + # With -q Maven prints its errors to stdout, which was captured above. + echo "ERROR: failed to read the project version and the local repository: ${PW_INFO}" + exit 1 +fi +read -r PW_VERSION LOCAL_REPO <<< "$PW_INFO" rm -rf dist # dist/m2: the playwright-java artifacts of this build, for the local Maven repository of From 9eddd35a9373cdef22a860f07f4e8bb6f1376a39 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 4 Nov 2026 11:22:40 -0800 Subject: [PATCH 7/7] fix(docker): do not bake the client into the image Like the other language ports, the image ships the runtime, the browsers and their dependencies; the client comes from the user's own build. The staged client jars are only mounted to run the CLI, and /root/.m2 is gone from the image. The Docker workflow copies the SNAPSHOT artifacts that build.sh installed on the runner into the container for the tests. --- .github/workflows/test_docker.yml | 12 +++++------- utils/docker/.dockerignore | 6 +++--- utils/docker/Dockerfile.jammy | 9 +++------ utils/docker/Dockerfile.noble | 9 +++------ utils/docker/Dockerfile.resolute | 9 +++------ utils/docker/build.sh | 28 +++++++++------------------- 6 files changed, 26 insertions(+), 47 deletions(-) diff --git a/.github/workflows/test_docker.yml b/.github/workflows/test_docker.yml index cd8f86606..b6f601177 100644 --- a/.github/workflows/test_docker.yml +++ b/.github/workflows/test_docker.yml @@ -57,13 +57,11 @@ jobs: - name: Copy repository inside docker container run: | docker cp . "$CONTAINER_ID":/home/pwuser/playwright - # /root/.m2 was populated as root during image build; move it to - # pwuser so the locally-installed SNAPSHOT artifacts resolve. - docker exec --user root "$CONTAINER_ID" bash -c ' - chown -R pwuser /home/pwuser/playwright - mv /root/.m2 /home/pwuser/.m2 - chown -R pwuser /home/pwuser/.m2 - ' + # build.sh installed the SNAPSHOT artifacts into the local Maven repository + # of the runner; give them to pwuser so the test projects resolve them. + docker exec "$CONTAINER_ID" mkdir -p /home/pwuser/.m2/repository/com/microsoft + docker cp ~/.m2/repository/com/microsoft/playwright "$CONTAINER_ID":/home/pwuser/.m2/repository/com/microsoft/playwright + docker exec --user root "$CONTAINER_ID" chown -R pwuser /home/pwuser/playwright /home/pwuser/.m2 - name: Run smoke tests in container run: | diff --git a/utils/docker/.dockerignore b/utils/docker/.dockerignore index 94c85dfae..c30d25e03 100644 --- a/utils/docker/.dockerignore +++ b/utils/docker/.dockerignore @@ -1,6 +1,6 @@ -# This directory is the Docker build context (see build.sh). The image reads only the -# artifacts staged in dist/ and the Maven tarball in maven/; keep everything else, such -# as the oras download of publish_docker.sh, out of the build context. +# This directory is the Docker build context (see build.sh). The image build reads only +# the client jars staged in dist/ and the Maven tarball in maven/; keep everything else, +# such as the oras download of publish_docker.sh, out of the build context. * !dist !maven diff --git a/utils/docker/Dockerfile.jammy b/utils/docker/Dockerfile.jammy index ea8085b43..fce58fc82 100644 --- a/utils/docker/Dockerfile.jammy +++ b/utils/docker/Dockerfile.jammy @@ -50,12 +50,9 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright # it into /tmp on every launch. See https://github.com/microsoft/playwright-java/issues/1268. ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver -# The playwright-java artifacts built by build.sh go into the local Maven repository so -# that Maven projects built in the image resolve them without downloading. -COPY dist/m2 /root/.m2/repository - -# The CLI runs from the jars staged by build.sh; the mount leaves nothing in the image. -RUN --mount=type=bind,source=dist/cli,target=/tmp/pw-java \ +# The client jar and its runtime dependencies, staged by build.sh, are only mounted to +# run the CLI; they do not go into the image. +RUN --mount=type=bind,source=dist,target=/tmp/pw-java \ mkdir /ms-playwright && \ java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install-driver && \ java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install --with-deps && \ diff --git a/utils/docker/Dockerfile.noble b/utils/docker/Dockerfile.noble index 4b2c480fe..07d4b8c3b 100644 --- a/utils/docker/Dockerfile.noble +++ b/utils/docker/Dockerfile.noble @@ -50,12 +50,9 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright # it into /tmp on every launch. See https://github.com/microsoft/playwright-java/issues/1268. ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver -# The playwright-java artifacts built by build.sh go into the local Maven repository so -# that Maven projects built in the image resolve them without downloading. -COPY dist/m2 /root/.m2/repository - -# The CLI runs from the jars staged by build.sh; the mount leaves nothing in the image. -RUN --mount=type=bind,source=dist/cli,target=/tmp/pw-java \ +# The client jar and its runtime dependencies, staged by build.sh, are only mounted to +# run the CLI; they do not go into the image. +RUN --mount=type=bind,source=dist,target=/tmp/pw-java \ mkdir /ms-playwright && \ java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install-driver && \ java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install --with-deps && \ diff --git a/utils/docker/Dockerfile.resolute b/utils/docker/Dockerfile.resolute index e89578635..58b0ff6ac 100644 --- a/utils/docker/Dockerfile.resolute +++ b/utils/docker/Dockerfile.resolute @@ -44,12 +44,9 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright # it into /tmp on every launch. See https://github.com/microsoft/playwright-java/issues/1268. ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver -# The playwright-java artifacts built by build.sh go into the local Maven repository so -# that Maven projects built in the image resolve them without downloading. -COPY dist/m2 /root/.m2/repository - -# The CLI runs from the jars staged by build.sh; the mount leaves nothing in the image. -RUN --mount=type=bind,source=dist/cli,target=/tmp/pw-java \ +# The client jar and its runtime dependencies, staged by build.sh, are only mounted to +# run the CLI; they do not go into the image. +RUN --mount=type=bind,source=dist,target=/tmp/pw-java \ mkdir /ms-playwright && \ java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install-driver && \ java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install --with-deps && \ diff --git a/utils/docker/build.sh b/utils/docker/build.sh index c951d9354..2a9acee81 100755 --- a/utils/docker/build.sh +++ b/utils/docker/build.sh @@ -11,7 +11,7 @@ if [[ ($1 == '--help') || ($1 == '-h') || ($1 == '') || ($2 == '') ]]; then echo " docker run --rm -it playwright:localbuild-noble /bin/bash" echo "" echo "NOTE: this requires a JDK, Maven and npm on the host: the client is built here" - echo "and the image only installs the result." + echo "and the image only runs it to install the browsers." echo "" exit 0 fi @@ -40,32 +40,22 @@ PW_TARGET_ARCH=$(echo $1 | cut -c3-) MVN=(mvn --no-transfer-progress) # Like the other language ports, build the client on the host, where npm, a JDK and -# Maven are available, and stage what the Dockerfile copies into the image under dist/. -# The build context is this directory, so nothing else reaches the image. +# Maven are available, and stage the client jar and its runtime dependencies under +# dist/. The Dockerfile runs the CLI from there to install the browsers and their +# dependencies. The build context is this directory, so nothing else reaches the image. ../../scripts/download_driver.sh "${MVN[@]}" -B install -D skipTests -f ../../pom.xml -if ! PW_INFO=$("${MVN[@]}" -q -N -f ../../pom.xml exec:exec -D exec.executable=echo \ - -D exec.args='${project.version} ${settings.localRepository}'); then +if ! PW_VERSION=$("${MVN[@]}" -q -N -f ../../pom.xml exec:exec -D exec.executable=echo \ + -D exec.args='${project.version}'); then # With -q Maven prints its errors to stdout, which was captured above. - echo "ERROR: failed to read the project version and the local repository: ${PW_INFO}" + echo "ERROR: failed to read the project version: ${PW_VERSION}" exit 1 fi -read -r PW_VERSION LOCAL_REPO <<< "$PW_INFO" rm -rf dist - -# dist/m2: the playwright-java artifacts of this build, for the local Maven repository of -# the image. Projects built in the image then resolve them without downloading. -for MODULE in parent-pom driver driver-bundle playwright; do - mkdir -p "dist/m2/com/microsoft/playwright/$MODULE" - cp -R "$LOCAL_REPO/com/microsoft/playwright/$MODULE/$PW_VERSION" "dist/m2/com/microsoft/playwright/$MODULE/" -done - -# dist/cli: the client jar and its runtime dependencies, which the Dockerfile runs the -# CLI from to install the driver, the browsers and their dependencies. "${MVN[@]}" -q -f ../../playwright/pom.xml dependency:copy-dependencies \ - -D includeScope=runtime -D outputDirectory="$PWD/dist/cli" -cp "../../playwright/target/playwright-$PW_VERSION.jar" dist/cli/ + -D includeScope=runtime -D outputDirectory="$PWD/dist" +cp "../../playwright/target/playwright-$PW_VERSION.jar" dist/ # The jammy and noble images install this Maven (see the Dockerfiles). MAVEN_VERSION=3.9.12