openpencil/Dockerfile.web-rust
Kayshen-X ace4c7257e feat(web): fetch preview assets at runtime instead of embedding them
The preview JPEGs (~2.4 MiB, already compressed so gzip passed them
straight through) leave the wasm data segment: a platform-free asset
registry in op-editor-core tracks per-route Absent/Pending/Ready/Failed
with single-flight and install-once semantics, the browser half fetches
over ArrayBuffer XHR with managed-mode headers and a slot-wrapped
callback so no synchronous failure can strand a route in Pending, and
paint sites fall back to the existing placeholder when bytes are not
(yet) there. Native keeps include_bytes verbatim. The staging script
copies the asset dirs into pkg/assets/ — under /pkg/ because the hub
frontend owns /assets/ — and the gate, CI workflow, and web image all
run it and assert the layout. Bundle: 7.13 → 5.10 MiB gzip, so the
tripwire returns to 6 MiB (85% occupancy); the sdk bundle keeps its own
8 MiB pending a real measurement.

Also closes the final review test gaps: the owner-session fixture now
returns a must-use lane guard (a dropped receiver made the saturated
lane read as Disconnected, not Full) and seeds the daemon's baseline
document so the hash check exercises the real path, and the closed
write barrier has a direct multi-page active-page regression test.
2026-08-09 01:37:06 +08:00

185 lines
9.6 KiB
Docker

# syntax=docker/dockerfile:1
#
# Dockerfile.web-rust — container for the RUST web host (C1).
#
# Serves the Rust CanvasKit web editor: the `op-host-web-server` daemon running
# in `--serve-web` mode, hosting the wasm-bindgen bundle + the vendored
# CanvasKit artifact out of a `web-bundle/` directory placed next to the binary.
# This is the sole web container now that the TypeScript/Nitro web app (and its
# root `Dockerfile`) have been retired.
#
# Build (self-contained, builds the wasm bundle in-image — the default):
# docker build -f Dockerfile.web-rust -t openpencil-web-rust .
#
# Build reusing a prebuilt bundle (the `op-web-bundle` artifact from the
# `wasm-bundle-build.yml` CI job — skips the in-image wasm rebuild):
# # download + unzip the CI artifact into ./web-bundle first, then:
# docker build -f Dockerfile.web-rust --build-arg WEB_BUNDLE_SOURCE=copy \
# -t openpencil-web-rust .
#
# Run (single-user, the historical default):
# docker run -p 3100:3100 openpencil-web-rust
# # then open http://localhost:3100/
#
# Run (multi-account online mode, behind a reverse proxy on one public origin):
# docker run -p 3100:3100 \
# -e OPENPENCIL_SERVE_MODE=online \
# -e OPENPENCIL_HUB_BASE_URL=http://backend:8080 \
# -e OPENPENCIL_HUB_INTERNAL_AUTH="$(cat /run/secrets/op-canvas-internal-auth)" \
# -e OPENPENCIL_WEB_ALLOWED_ORIGINS=https://hub.example.com \
# openpencil-web-rust
# Every request is then attributed to a verified op-hub account and served
# against that account's own document. See `web_canvas_server/online_policy.rs`
# for what online mode refuses, and `hub_auth_client.rs` for the identity call.
#
# Why wasm-in-Docker is the default (vs. always COPY-artifact): the production
# `canvaskit` feature is pure Rust + web_sys + serde — it needs NO emscripten /
# EMSDK / skia-safe / libc shim (the from-scratch skia path that needed those
# was retired 2026-06-17; the editor renders through the official CanvasKit
# skia WASM loaded separately). So the only extra toolchain over a normal cargo
# build is the wasm32 target + wasm-bindgen-cli + pinned Binaryen (wasm-opt) +
# node, all installed in the builder stage. That keeps the image self-contained
# and reproducible. The `WEB_BUNDLE_SOURCE=copy` build arg is the
# fast path: it skips the wasm rebuild and copies a `./web-bundle` provided as
# build context (e.g. the Task-1 CI artifact).
# ── Stage 1: builder ──────────────────────────────────────────────────────────
# rust:1.94 ships cargo + a Debian (bookworm) base. Debian's apt binaryen is
# too old for rustc 1.94's wasm feature set, so wasm-opt is pinned below to the
# same modern Binaryen release used by the wasm bundle CI.
FROM rust:1.94-bookworm AS builder
# WEB_BUNDLE_SOURCE = build -> build the wasm bundle in this stage (default).
# = copy -> skip the build; the runtime stage COPYs a
# prebuilt ./web-bundle from the build context.
ARG WEB_BUNDLE_SOURCE=build
ARG BINARYEN_VERSION=version_123
# Port baked into the image's CMD; overridable at build + run time.
ARG SERVE_PORT=3100
# pinned Binaryen -> wasm-opt -Oz ; nodejs -> the 0-env-import assert in the gate
# script. op-host-web-server is the headless raster daemon (links op-host-services
# only — no winit/glutin/skia-GL under Approach Y), so NONE of the GL/X11
# link-time libs the desktop binary needed are required here; only
# freetype/fontconfig (+ CJK fonts) for skia's raster text shaping at export.
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
nodejs \
gzip \
ca-certificates \
pkg-config \
libfreetype-dev libfontconfig1-dev fonts-noto-cjk \
&& rm -rf /var/lib/apt/lists/* \
&& curl -fsSL \
"https://github.com/WebAssembly/binaryen/releases/download/${BINARYEN_VERSION}/binaryen-${BINARYEN_VERSION}-x86_64-linux.tar.gz" \
| tar -xz -C /opt \
&& ln -s "/opt/binaryen-${BINARYEN_VERSION}/bin/wasm-opt" /usr/local/bin/wasm-opt \
&& wasm-opt --version
WORKDIR /src
# Copy the whole repo (the `.dockerignore` trims node_modules / out / dist etc.)
# Submodules (vendor/jian) must already be checked out in the build context —
# the CI checkout uses `submodules: recursive`.
COPY . .
# Build the headless web/MCP server binary (the `--serve-web` host) — links
# op-host-services only, no winit/skia-GL. Always built; this is the runtime
# binary regardless of how the web bundle is produced.
RUN cargo build -p op-host-web-server --release
# Build the canvaskit wasm bundle in-image, UNLESS WEB_BUNDLE_SOURCE=copy.
# Mirrors `tools/check-wasm-bundle.sh` exactly (cargo build --features canvaskit
# -> wasm-bindgen --target web -> 0-env-import assert -> wasm-opt -Oz -> gzip
# size gate). The script reads the locked wasm-bindgen-cli version requirement
# from Cargo.lock so the CLI matches the linked runtime.
RUN if [ "$WEB_BUNDLE_SOURCE" = "build" ]; then \
rustup target add wasm32-unknown-unknown && \
version="$(awk '/^name = "wasm-bindgen"$/{found=1; next} found && /^version = /{gsub(/[" ]/,"",$3); print $3; exit}' Cargo.lock)" && \
if [ -z "$version" ]; then echo "could not resolve wasm-bindgen version from Cargo.lock" >&2; exit 2; fi && \
cargo install wasm-bindgen-cli --version "$version" --locked && \
bash tools/check-wasm-bundle.sh ; \
else \
echo "WEB_BUNDLE_SOURCE=$WEB_BUNDLE_SOURCE — skipping in-image wasm build; runtime stage will COPY ./web-bundle from the build context" ; \
fi
# Assemble the deployable web-bundle/ layout the daemon's `web_static.rs`
# resolves: the wasm-bindgen `pkg/` output (which now also carries the runtime
# product assets under `assets/`, staged by `tools/stage-web-assets.sh` inside
# the gate script) PLUS the vendored CanvasKit artifact under a `canvaskit/`
# subdir. For the `copy` path the bundle already exists in the build context
# (./web-bundle); just normalize it into /out/web-bundle so the runtime stage
# has one stable source path either way.
RUN mkdir -p /out/web-bundle && \
if [ "$WEB_BUNDLE_SOURCE" = "build" ]; then \
cp -R crates/op-host-web/pkg/. /out/web-bundle/ && \
cp -R crates/op-host-web/assets/canvaskit /out/web-bundle/canvaskit ; \
else \
cp -R web-bundle/. /out/web-bundle/ ; \
fi && \
echo "assembled web-bundle:" && find /out/web-bundle -maxdepth 2 -type f | sort && \
# The browser fetches preview JPEGs from `/pkg/assets/…` at runtime (see
# `op_editor_core::web_assets`), so a bundle without them degrades every
# card to a placeholder. Catch it here rather than in production — the
# `copy` path is the likely offender, fed a pre-split artifact.
if [ ! -d /out/web-bundle/assets/prompt_center_previews ]; then \
echo "FAIL: web-bundle is missing assets/prompt_center_previews (stale artifact?)" >&2; exit 1; \
fi
# ── Stage 2: runtime (slim) ───────────────────────────────────────────────────
# Only the runtime shared libs the daemon dlopens at run time (GL / fontconfig /
# freetype + CJK fonts). No Rust toolchain, no node, no build deps.
FROM debian:bookworm-slim AS runtime
ARG SERVE_PORT=3100
# `local` keeps the historical single-user daemon; `online` adds `--online`,
# the multi-account mode. Set at run time, so one image serves both.
ARG SERVE_MODE=local
ENV OPENPENCIL_SERVE_PORT=${SERVE_PORT} \
OPENPENCIL_SERVE_MODE=${SERVE_MODE}
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates wget \
libfreetype6 libfontconfig1 fonts-noto-cjk \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd -g 10001 openpencil \
&& useradd -r -u 10001 -g openpencil -M -d /app openpencil
WORKDIR /app
# Lay out the bundle the way `web_static.rs` expects: the daemon resolves the
# bundle as `<exe_dir>/web-bundle` (and CanvasKit as `<exe_dir>/web-bundle/
# canvaskit`). Keep the binary + the bundle dir as siblings under /app so that
# `<exe_dir> == /app`.
COPY --from=builder --chown=10001:10001 /src/target/release/op-host-web-server /app/op-host-web-server
COPY --from=builder --chown=10001:10001 /out/web-bundle /app/web-bundle
# The daemon writes nothing outside /tmp in online mode (settings persistence
# is refused there), so it runs unprivileged and the image can be mounted
# read-only by the orchestrator.
# The online data directory, created with the runtime owner so a fresh named
# volume inherits that ownership and mode on its first mount. The daemon probes
# it at start-up and refuses to run if it is not writable.
RUN install -d -o 10001 -g 10001 -m 0700 /data
USER 10001:10001
EXPOSE ${SERVE_PORT}
# `/` is the one route served without a credential in every mode, so it is the
# only probe that works for both `local` and `online`.
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=6 \
CMD wget --quiet --output-document=/dev/null \
"http://127.0.0.1:${OPENPENCIL_SERVE_PORT}/" || exit 1
STOPSIGNAL SIGTERM
# Bind 0.0.0.0 so the daemon is reachable from outside the container (the LAN /
# Docker opt-in documented in `parse_serve_web_args`). No TLS — front with a
# reverse proxy for anything beyond a trusted network. The port is taken from
# the build-time SERVE_PORT (baked into OPENPENCIL_SERVE_PORT); `sh -c` lets the
# env var expand at container start.
CMD ["sh", "-c", "if [ \"${OPENPENCIL_SERVE_MODE}\" = online ]; then set -- --online; else set --; fi; \
exec /app/op-host-web-server --serve-web \"${OPENPENCIL_SERVE_PORT}\" --host 0.0.0.0 \"$@\""]