diff --git a/bridges/remote-renderer/Dockerfile b/bridges/remote-renderer/Dockerfile
index 1573688..8ec938d 100644
--- a/bridges/remote-renderer/Dockerfile
+++ b/bridges/remote-renderer/Dockerfile
@@ -1,9 +1,22 @@
-# Build only on an operator-approved, immutable existing renderer image.
-# This file does not build/download the engine or confer isolation qualification.
+# Build only on an operator-approved, immutable existing renderer image (RENDERER_IMAGE, the engine image from
+# renderer/Dockerfile). This file does not build the engine or confer isolation qualification.
+# The service is the RELEASED transport artifact, resolved at the exact TRANSPORT_VERSION from the QRun-IO Gitea Maven
+# registry and Maven Central (no SNAPSHOT, no local build, no default). BUILD_IMAGE must be an immutable digest.
+# docker build --build-arg BUILD_IMAGE=... --build-arg RENDERER_IMAGE=... --build-arg TRANSPORT_VERSION=1.0.0 bridges/remote-renderer/
+ARG BUILD_IMAGE
ARG RENDERER_IMAGE
+FROM ${BUILD_IMAGE} AS release
+ARG TRANSPORT_VERSION
+WORKDIR /release
+COPY release-image/pom.xml ./pom.xml
+RUN test -n "${TRANSPORT_VERSION}" && case "${TRANSPORT_VERSION}" in *SNAPSHOT*|*forge*) echo "TRANSPORT_VERSION must be a released version" >&2; exit 1;; esac \
+ && mvn -B -Dtransport.version="${TRANSPORT_VERSION}" -Dmaven.repo.local=/release/.m2 package \
+ && mv "target/lib/qqq-remote-renderer-transport-${TRANSPORT_VERSION}.jar" target/service.jar \
+ && test -f target/service.jar && ls target/lib
+
FROM ${RENDERER_IMAGE}
-COPY transport/target/qqq-remote-renderer-transport-0.1.0-SNAPSHOT.jar /remote/service.jar
-COPY transport/target/lib/ /remote/lib/
+COPY --from=release /release/target/service.jar /remote/service.jar
+COPY --from=release /release/target/lib/ /remote/lib/
COPY LICENSE NOTICE /usr/share/doc/qqq-remote-renderer/
USER 65532:65532
EXPOSE 18445
diff --git a/bridges/remote-renderer/release-image/pom.xml b/bridges/remote-renderer/release-image/pom.xml
new file mode 100644
index 0000000..00acb08
--- /dev/null
+++ b/bridges/remote-renderer/release-image/pom.xml
@@ -0,0 +1,21 @@
+
+
+
+ 4.0.0
+ com.kingsrook.qbitsqqq-remote-renderer-image0pom
+
+ centralhttps://repo.maven.apache.org/maven2false
+ qrun-iohttps://git.kof22.com/api/packages/QRun-IO/mavenfalsefail
+
+
+ com.kingsrook.qbitsqqq-remote-renderer-transport${transport.version}
+
+
+ org.apache.maven.pluginsmaven-dependency-plugin3.9.0
+ release-librariespackagecopy-dependencies
+ runtime${project.build.directory}/lib
+
+
+
diff --git a/renderer/Dockerfile b/renderer/Dockerfile
index 578ffba..d91053f 100644
--- a/renderer/Dockerfile
+++ b/renderer/Dockerfile
@@ -10,18 +10,18 @@ COPY Dockerfile ./
RUN apt-get update && apt-get install -y --no-install-recommends gcc g++ libc6-dev make cmake patch \
&& sh ./build-zint.sh
-# The renderer's version comes from its parent, the repository reactor POM (../pom.xml). Build from this directory with that file as a named BuildKit context:
-# docker build --build-context reactor= ... (the reactor POM is copied to /pom.xml, where ../pom.xml resolves for /renderer/pom.xml)
-FROM ${BUILD_IMAGE} AS build
-COPY --from=reactor pom.xml /pom.xml
-WORKDIR /renderer
-COPY vendor/openhtml-1.1.105-qqq-p1 ./vendor/openhtml-1.1.105-qqq-p1
-RUN sh ./vendor/openhtml-1.1.105-qqq-p1/build-openhtml.sh /renderer/openhtml-build /renderer/.m2
-COPY pom.xml ./
-COPY LICENSE NOTICE ./
-COPY checkstyle ./checkstyle
-COPY src ./src
-RUN mvn -B -Dmaven.repo.local=/renderer/.m2 verify
+# The engine and its libraries are the RELEASED artifacts, resolved at the exact RENDERER_VERSION from the QRun-IO
+# Gitea Maven registry and Maven Central (Maven checks each registry checksum). No SNAPSHOT, no local build and no
+# local-cache coordinate: the version is a required build argument with no default.
+# docker build --build-arg BUILD_IMAGE=... --build-arg JAVA_IMAGE=... --build-arg RENDERER_VERSION=1.0.0 renderer/
+FROM ${BUILD_IMAGE} AS release
+ARG RENDERER_VERSION
+WORKDIR /release
+COPY release-image/pom.xml ./pom.xml
+RUN test -n "${RENDERER_VERSION}" && case "${RENDERER_VERSION}" in *SNAPSHOT*|*forge*) echo "RENDERER_VERSION must be a released version" >&2; exit 1;; esac \
+ && mvn -B -Drenderer.version="${RENDERER_VERSION}" -Dmaven.repo.local=/release/.m2 package \
+ && mv "target/lib/qbit-document-renderer-${RENDERER_VERSION}.jar" target/renderer.jar \
+ && test -f target/renderer.jar && ls target/lib
FROM ${JAVA_IMAGE}
# Reviewed runtime family: Temurin 21.0.13+9 on Ubuntu noble. The approved digest
@@ -37,8 +37,9 @@ RUN . /etc/os-release \
WORKDIR /renderer
COPY --from=zint-build /zint-build/build/frontend/zint /usr/local/bin/zint
COPY --from=zint-build /zint-build/corresponding-source /usr/share/doc/qqq-zint
-COPY --from=build /renderer/openhtml-build/corresponding-source /usr/share/doc/qqq-openhtml
-COPY --from=build /renderer/target/qbit-document-renderer-0.1.0-SNAPSHOT.jar ./renderer.jar
-COPY --from=build /renderer/target/lib ./lib
+# LGPL corresponding source for the vendored OpenHTML module that the released openhtmltopdf-pdfbox jar was built from.
+COPY vendor/openhtml-1.1.105-qqq-p1 /usr/share/doc/qqq-openhtml
+COPY --from=release /release/target/renderer.jar ./renderer.jar
+COPY --from=release /release/target/lib ./lib
USER 65532:65532
ENTRYPOINT ["java", "-Xmx256m", "-XX:ActiveProcessorCount=1", "-Djava.awt.headless=true", "-Dpdfbox.fontcache=/tmp", "-jar", "/renderer/renderer.jar"]
diff --git a/renderer/IMAGE-PUBLISH.md b/renderer/IMAGE-PUBLISH.md
new file mode 100644
index 0000000..652ca9d
--- /dev/null
+++ b/renderer/IMAGE-PUBLISH.md
@@ -0,0 +1,120 @@
+# Renderer container images: build from a released version and publish
+
+Two images, built in this order. Both are built from **released** Maven artifacts (QRun-IO Gitea registry
+`https://git.kof22.com/api/packages/QRun-IO/maven` and Maven Central); there is no SNAPSHOT, local-cache or reactor
+input. The version is a required build argument with no default, and the Dockerfiles refuse a `SNAPSHOT` or `forge`
+version.
+
+| Image | Dockerfile (context) | Version argument | Contents |
+|---|---|---|---|
+| `ghcr.io/gg-engineering/qqq-document-renderer` | `renderer/Dockerfile` (`renderer/`) | `RENDERER_VERSION` (`qbit-document-renderer`) | engine, its libraries, patched Zint built from `renderer/vendor`, Temurin runtime |
+| `ghcr.io/gg-engineering/qqq-remote-renderer` | `bridges/remote-renderer/Dockerfile` (`bridges/remote-renderer/`) | `TRANSPORT_VERSION` (`qqq-remote-renderer-transport`), `RENDERER_IMAGE` | the mTLS service, on top of the engine image |
+
+The service image is the one Forge deploys. The adapter (`qbit-document-generation-remote-renderer`) is a client-side
+library and is not in either image.
+
+## Version choice
+
+`1.0.0` is the released, non-RC version and the one `gg-forge` pins for the document-generation family. `1.1.0-RC.1`
+contains the same `renderer/` and `bridges/remote-renderer/` sources as `1.0.0` (only POM/CI alignment differs), so it
+adds nothing for the renderer and is a release candidate. Build the version the host library pins.
+
+## Reviewed base images
+
+`BUILD_IMAGE` and `JAVA_IMAGE` must be immutable digests. `JAVA_IMAGE` is the reviewed runtime in `DEPLOYMENT.md`.
+`BUILD_IMAGE` only resolves artifacts; no layer of it reaches the final image.
+
+```sh
+BUILD_IMAGE=maven@sha256:99e61abcff91a9b1333463bd8451fb18495d6eba9250ac66a338b518f8278320 # maven:3.9-eclipse-temurin-21
+JAVA_IMAGE=eclipse-temurin@sha256:db724162e4ff5f71fa0a2815026314d9e5e0e9a139f49be1d4717e6930e93341 # 21.0.13_9-jre-noble (amd64)
+VERSION=1.0.0
+```
+
+## Build, smoke, push
+
+Disable BuildKit provenance and SBOM attestations so each tag is a single OCI image manifest (the digest the chart pins).
+
+```sh
+docker build --provenance=false --sbom=false --build-arg BUILD_IMAGE=$BUILD_IMAGE --build-arg JAVA_IMAGE=$JAVA_IMAGE --build-arg RENDERER_VERSION=$VERSION -t qqq-document-renderer:$VERSION renderer
+docker build --provenance=false --sbom=false --build-arg BUILD_IMAGE=$BUILD_IMAGE --build-arg RENDERER_IMAGE=qqq-document-renderer:$VERSION --build-arg TRANSPORT_VERSION=$VERSION -t qqq-remote-renderer:$VERSION bridges/remote-renderer
+
+# Optional scan of the final image
+docker save qqq-remote-renderer:$VERSION -o svc.tar
+docker run --rm -v "$PWD/svc.tar:/svc.tar:ro" aquasec/trivy:latest image --input /svc.tar --severity HIGH,CRITICAL --scanners vuln
+
+# Publish (docker must be logged in to ghcr.io with a token that has write:packages)
+for n in qqq-document-renderer qqq-remote-renderer; do
+ docker tag $n:$VERSION ghcr.io/gg-engineering/$n:$VERSION
+ docker push ghcr.io/gg-engineering/$n:$VERSION # prints the manifest digest
+done
+```
+
+New GHCR packages are private. Package access is per package and is managed in the GitHub UI (Package settings,
+Manage access); the REST API cannot grant it. The credential behind `ghcr-pull-secret` must be able to read both packages.
+
+## Pins for gg-forge-cd
+
+`sh renderer/image-pins.sh ghcr.io/gg-engineering/qqq-remote-renderer@sha256:` pulls the published
+image and prints the `echoRenderer.*` image, classpath and file-hash pins from the image itself. `imageId` is
+`docker image inspect --format '{{.Id}}'`; with the containerd image store this equals the manifest digest, otherwise
+it is the config digest. Whatever value is chosen must be the same on Forge and the service.
+
+Values for `1.0.0` as published (the java, zint and patchset hashes match the 2026-11-05 local qualification; the
+engine JAR and the OpenHTML JAR differ because they are the released artifacts):
+
+```yaml
+echoRenderer:
+ image:
+ repository: ghcr.io/gg-engineering/qqq-remote-renderer
+ digest: sha256:4152a4253e543068bedfd438775ed5327807bdddf679bc5a8e6eab26855e09ea
+ imageId: sha256:4152a4253e543068bedfd438775ed5327807bdddf679bc5a8e6eab26855e09ea
+ classpath:
+ - /renderer/renderer.jar
+ - /renderer/lib/commons-logging-1.4.0.jar
+ - /renderer/lib/fontbox-3.0.8.jar
+ - /renderer/lib/freemarker-2.3.35.jar
+ - /renderer/lib/graphics2d-3.0.1.jar
+ - /renderer/lib/jackson-annotations-2.22.jar
+ - /renderer/lib/jackson-core-2.22.4.jar
+ - /renderer/lib/jackson-databind-2.22.4.jar
+ - /renderer/lib/jsoup-1.23.2.jar
+ - /renderer/lib/okapibarcode-0.5.7.jar
+ - /renderer/lib/openhtmltopdf-core-1.1.105.jar
+ - /renderer/lib/openhtmltopdf-pdfbox-1.0.0.jar
+ - /renderer/lib/pdfbox-3.0.8.jar
+ - /renderer/lib/pdfbox-io-3.0.8.jar
+ - /renderer/lib/slf4j-api-2.0.17.jar
+ - /renderer/lib/slf4j-nop-2.0.17.jar
+ - /renderer/lib/xmpbox-3.0.8.jar
+ fileHashes:
+ /opt/java/openjdk/bin/java: 7afc28801f744050aeba646b20e7314265279f76f9d0273b21a2b66411e1bf6d
+ /renderer/renderer.jar: 0061e1bce631e468b0d7d08735bb764ba2b6d85260c79c144beabad7ffbc19db
+ /usr/local/bin/zint: ea8dc40c917abd7e8ccc824ef0adafed245a2425abbdfac6a30faeb8e4642233
+ /usr/share/doc/qqq-zint/patchset-id.txt: a287946580d5eeb8691ac1d98d6a7f50bf22dcc5dc0ef039d7033e76e15c52d2
+ /renderer/lib/commons-logging-1.4.0.jar: d175dbd751dd782a63bde28c7a039520e971f25e84b79c19b8435edc3603e0dc
+ /renderer/lib/fontbox-3.0.8.jar: a1915c24e3edbe0ecec93896dfbf6d41427810b663ade97bd4e8bae86ec3fdab
+ /renderer/lib/freemarker-2.3.35.jar: 0fac87dddd78f1223139e8ef88e819c7f483c0a3835cdf5982ad5e4576d1d896
+ /renderer/lib/graphics2d-3.0.1.jar: 4849aaa6f536d75dea1b835a71086702216787036f42716adfad438c6e7928c0
+ /renderer/lib/jackson-annotations-2.22.jar: 21ddb598807d3a51a876704eb979d9296e1c6a6f47ab1826ff88c6d6a127a2d0
+ /renderer/lib/jackson-core-2.22.4.jar: 2d30c375cddea9657a1e1ef97547a2694b663b355facb4de53a097782b312d1a
+ /renderer/lib/jackson-databind-2.22.4.jar: d4cbf8947087f305869826801913abdc50735a1fab2074a1993e7bc67ca974a3
+ /renderer/lib/jsoup-1.23.2.jar: e9d8c856856680427f096d156f02a414289985c930998fcb519ce5be39b42003
+ /renderer/lib/okapibarcode-0.5.7.jar: f33b7d17b7040aa1d9c23b1f3bb274e492b786c340b02d9557bcc027ac874355
+ /renderer/lib/openhtmltopdf-core-1.1.105.jar: d5951a8407c3be41108698066e6d67cf952e526be39d4e49b0b9fbcee761de7d
+ /renderer/lib/openhtmltopdf-pdfbox-1.0.0.jar: ec2865bfa45513e63adaa1a3c6387eb6bbd481e6b23f4d68a33cfedfc753cfe8
+ /renderer/lib/pdfbox-3.0.8.jar: 97647cfbde61ebcfc06b4cf8c9b0ffcaaee073396eceb4a7f6836a9b9128903c
+ /renderer/lib/pdfbox-io-3.0.8.jar: 36a0e04001010b4c764857817412b96339930b19755e728959805cc0352061b2
+ /renderer/lib/slf4j-api-2.0.17.jar: 7b751d952061954d5abfed7181c1f645d336091b679891591d63329c622eb832
+ /renderer/lib/slf4j-nop-2.0.17.jar: 3716f83649ec66161a2edefd4f49df34d1dd1c51cdcf941996c6987260f0a829
+ /renderer/lib/xmpbox-3.0.8.jar: f044d2e316e57714a0adf272ad8abf4a863b8978fd17dc5b86dcfa8135469b10
+```
+
+Not covered by these pins: `identity.*`, `certificateNamespaces`, TLS identities, and chart switches.
+
+## What was and was not verified (1.0.0 build)
+
+* Verified: `renderer` (174) and transport (19) tests and Checkstyle pass on this source (same sources as the `v1.0.0` tag);
+ both images build; the service image runs read-only, non-root, all capabilities dropped, and served CATALOG, PDF
+ (Code128, GS1-128, Zint DotCode) and ZPL renders over a real mutual-TLS handshake with ephemeral certificates.
+* Not verified: no cluster run, no handshake against Forge, no gVisor or hostile-input qualification (the service is
+ `TRUSTED_LAB` only), no byte-for-byte rebuild of the OpenHTML jar from `vendor/`.
diff --git a/renderer/image-pins.sh b/renderer/image-pins.sh
new file mode 100644
index 0000000..dd8f3ed
--- /dev/null
+++ b/renderer/image-pins.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+# Print the gg-forge-cd echoRenderer.* pins (non-secret) for a PUBLISHED service image, read from the image itself:
+# sh renderer/image-pins.sh ghcr.io/gg-engineering/qqq-remote-renderer@sha256:
+# The classpath is /renderer/renderer.jar followed by /renderer/lib/*.jar in byte order; hashes are SHA256 of the files in the image.
+set -eu
+image=${1:?image reference by digest required}
+case "$image" in *@sha256:*) ;; *) echo 'Reference the image by @sha256: digest' >&2; exit 2;; esac
+repository=${image%@*}
+digest=${image#*@}
+docker pull "$image" >/dev/null
+image_id=$(docker image inspect "$image" --format '{{.Id}}')
+echo "echoRenderer:"
+echo " image:"
+echo " repository: $repository"
+echo " digest: $digest"
+echo " imageId: $image_id"
+docker run --rm --pull never --network none --user 65532:65532 --entrypoint sh "$image" -c '
+ set -eu
+ echo " classpath:"
+ echo " - /renderer/renderer.jar"
+ for f in $(LC_ALL=C ls /renderer/lib/*.jar); do echo " - $f"; done
+ echo " fileHashes:"
+ for f in /opt/java/openjdk/bin/java /renderer/renderer.jar /usr/local/bin/zint /usr/share/doc/qqq-zint/patchset-id.txt $(LC_ALL=C ls /renderer/lib/*.jar); do
+ printf " %s: %s\n" "$f" "$(sha256sum "$f" | cut -d" " -f1)"
+ done'
diff --git a/renderer/release-image/pom.xml b/renderer/release-image/pom.xml
new file mode 100644
index 0000000..559b348
--- /dev/null
+++ b/renderer/release-image/pom.xml
@@ -0,0 +1,21 @@
+
+
+
+ 4.0.0
+ com.kingsrook.qbitsqbit-document-renderer-image0pom
+
+ centralhttps://repo.maven.apache.org/maven2false
+ qrun-iohttps://git.kof22.com/api/packages/QRun-IO/mavenfalsefail
+
+
+ com.kingsrook.qbitsqbit-document-renderer${renderer.version}
+
+
+ org.apache.maven.pluginsmaven-dependency-plugin3.9.0
+ release-librariespackagecopy-dependencies
+ runtime${project.build.directory}/lib
+
+
+