# 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: # docker run -p 3100:3100 openpencil-web-rust # # then open http://localhost:3100/ # # 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 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 # ── 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 ENV OPENPENCIL_SERVE_PORT=${SERVE_PORT} RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ libfreetype6 libfontconfig1 fonts-noto-cjk \ && rm -rf /var/lib/apt/lists/* WORKDIR /app # Lay out the bundle the way `web_static.rs` expects: the daemon resolves the # bundle as `/web-bundle` (and CanvasKit as `/web-bundle/ # canvaskit`). Keep the binary + the bundle dir as siblings under /app so that # ` == /app`. COPY --from=builder /src/target/release/op-host-web-server /app/op-host-web-server COPY --from=builder /out/web-bundle /app/web-bundle EXPOSE ${SERVE_PORT} # 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", "exec /app/op-host-web-server --serve-web \"${OPENPENCIL_SERVE_PORT}\" --host 0.0.0.0"]