openpencil/Dockerfile.web-rust

172 lines
8.7 KiB
Docker
Raw Normal View History

2026-06-18 21:00:25 +00:00
# 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
2026-06-18 21:00:25 +00:00
# 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.
2026-06-18 21:00:25 +00:00
#
# 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):
2026-06-18 21:00:25 +00:00
# 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.
#
2026-06-18 21:00:25 +00:00
# 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
2026-06-18 21:00:25 +00:00
# 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.
2026-06-18 21:00:25 +00:00
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
2026-06-18 21:00:25 +00:00
# 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.
2026-06-18 21:00:25 +00:00
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
2026-06-18 21:00:25 +00:00
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
2026-06-18 21:00:25 +00:00
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
2026-06-18 21:00:25 +00:00
# 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 && \
2026-06-18 21:00:25 +00:00
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
# `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}
2026-06-18 21:00:25 +00:00
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates wget \
2026-06-18 21:00:25 +00:00
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
2026-06-18 21:00:25 +00:00
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.
USER 10001:10001
2026-06-18 21:00:25 +00:00
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
2026-06-18 21:00:25 +00:00
# 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", "set -- --serve-web \"${OPENPENCIL_SERVE_PORT}\" --host 0.0.0.0; \
if [ \"${OPENPENCIL_SERVE_MODE}\" = online ]; then set -- \"$@\" --online; fi; \
exec /app/op-host-web-server \"$@\""]