From 26e5e4cf02073bfce7207b425e8634342b0b0d76 Mon Sep 17 00:00:00 2001 From: Henry Park Date: Thu, 9 Apr 2026 13:39:29 -0700 Subject: [PATCH] feat(docker): pre-bundle WASM extensions in staging image (#2210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(docker): pre-bundle WASM extensions in staging image Add a wasm-builder Docker stage that builds all registry tool/channel extensions from source and copies the .wasm + .capabilities.json files into the staging runtime image. Production images are unaffected — Docker only builds the wasm-builder stage when --target runtime-staging is used. The docker.yml workflow passes --target runtime-staging for scheduled (staging) builds and workflow_dispatch with tag=staging, while all other builds use --target runtime (no extensions). Co-Authored-By: Claude Opus 4.6 (1M context) * fix(docker): address PR review feedback - Use COPY --chown instead of separate RUN chown layer (fewer layers) - Reorder stages so runtime (production) is last — bare docker build defaults to production, not staging - Use --locked when Cargo.lock is present for reproducible WASM builds Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .dockerignore | 1 - .github/workflows/docker.yml | 8 +++++ Dockerfile | 59 ++++++++++++++++++++++++++++++++++-- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/.dockerignore b/.dockerignore index 32b9468cab..09a7c89ec5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,4 +5,3 @@ target/ *.md !CLAUDE.md node_modules/ -tools-src/ diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 9c6bfacb5e..5400efdfdc 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -85,6 +85,13 @@ jobs: echo "tags=${TAGS}" >> "$GITHUB_OUTPUT" echo "worker_tags=${WORKER_TAGS}" >> "$GITHUB_OUTPUT" + # Staging builds get pre-bundled WASM extensions + if [[ "${EVENT_NAME}" == "schedule" || "${INPUT_TAG}" == "staging" ]]; then + echo "target=runtime-staging" >> "$GITHUB_OUTPUT" + else + echo "target=runtime" >> "$GITHUB_OUTPUT" + fi + - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 @@ -100,6 +107,7 @@ jobs: context: . push: true tags: ${{ steps.tags.outputs.tags }} + target: ${{ steps.tags.outputs.target }} platforms: linux/amd64 cache-from: type=gha cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index 89b9366597..4d74c872b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,6 +32,7 @@ COPY tests/ tests/ COPY migrations/ migrations/ COPY registry/ registry/ COPY channels-src/ channels-src/ +COPY tools-src/ tools-src/ COPY wit/ wit/ COPY providers.json providers.json @@ -59,13 +60,56 @@ COPY tests/ tests/ COPY migrations/ migrations/ COPY registry/ registry/ COPY channels-src/ channels-src/ +COPY tools-src/ tools-src/ COPY wit/ wit/ COPY providers.json providers.json RUN cargo build --profile dist --bin ironclaw -# Stage 5: Minimal runtime -FROM debian:bookworm-slim +# Stage 4b: Build all WASM extensions from source (only used by runtime-staging) +FROM builder AS wasm-builder + +RUN apt-get update && apt-get install -y --no-install-recommends jq && rm -rf /var/lib/apt/lists/* + +RUN set -eux; \ + mkdir -p /app/wasm-bundles/tools /app/wasm-bundles/channels; \ + for manifest in registry/tools/*.json registry/channels/*.json; do \ + [ -f "$manifest" ] || continue; \ + kind=$(jq -r '.kind' "$manifest"); \ + ext_name=$(jq -r '.name' "$manifest"); \ + source_dir=$(jq -r '.source.dir' "$manifest"); \ + caps_file=$(jq -r '.source.capabilities' "$manifest"); \ + crate_name=$(jq -r '.source.crate_name' "$manifest"); \ + [ -d "$source_dir" ] || continue; \ + # Telegram is embedded in the binary at build time; skip it + [ "$ext_name" = "telegram" ] && continue; \ + echo "=== Building $ext_name from $source_dir ==="; \ + if [ -f "$source_dir/Cargo.lock" ]; then \ + CARGO_TARGET_DIR=/app/target cargo build --locked --release --target wasm32-wasip2 \ + --manifest-path "$source_dir/Cargo.toml" || { echo "WARN: build failed for $ext_name"; continue; }; \ + else \ + CARGO_TARGET_DIR=/app/target cargo build --release --target wasm32-wasip2 \ + --manifest-path "$source_dir/Cargo.toml" || { echo "WARN: build failed for $ext_name"; continue; }; \ + fi; \ + wasm_artifact=$(echo "${crate_name}" | tr '-' '_'); \ + raw_wasm="/app/target/wasm32-wasip2/release/${wasm_artifact}.wasm"; \ + [ -f "$raw_wasm" ] || continue; \ + dest_dir="/app/wasm-bundles/tools"; \ + [ "$kind" = "channel" ] && dest_dir="/app/wasm-bundles/channels"; \ + wasm-tools component new "$raw_wasm" -o "$dest_dir/${ext_name}.wasm" 2>/dev/null \ + || cp "$raw_wasm" "$dest_dir/${ext_name}.wasm"; \ + wasm-tools strip "$dest_dir/${ext_name}.wasm" -o "$dest_dir/${ext_name}.wasm.tmp" 2>/dev/null \ + && mv "$dest_dir/${ext_name}.wasm.tmp" "$dest_dir/${ext_name}.wasm" \ + || true; \ + [ -f "$source_dir/$caps_file" ] && cp "$source_dir/$caps_file" "$dest_dir/${ext_name}.capabilities.json"; \ + echo " -> $dest_dir/${ext_name}.wasm"; \ + done; \ + count=$(find /app/wasm-bundles -name '*.wasm' | wc -l); \ + echo "Built $count WASM extensions"; \ + [ "$count" -gt 0 ] || { echo "ERROR: No WASM extensions were built"; exit 1; } + +# Stage 5a: Shared runtime base +FROM debian:bookworm-slim AS runtime-base RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates \ @@ -80,10 +124,19 @@ RUN useradd -m -d /home/ironclaw -u 1000 ironclaw \ && mkdir -p /home/ironclaw/.ironclaw \ && chown -R ironclaw:ironclaw /home/ironclaw WORKDIR /home/ironclaw -USER ironclaw EXPOSE 3000 ENV RUST_LOG=ironclaw=info ENTRYPOINT ["ironclaw"] + +# Stage 5b: Staging runtime (with pre-built WASM extensions) +FROM runtime-base AS runtime-staging +COPY --from=wasm-builder --chown=ironclaw:ironclaw /app/wasm-bundles/tools/ /home/ironclaw/.ironclaw/tools/ +COPY --from=wasm-builder --chown=ironclaw:ironclaw /app/wasm-bundles/channels/ /home/ironclaw/.ironclaw/channels/ +USER ironclaw + +# Stage 5c: Production runtime (default — no pre-bundled extensions) +FROM runtime-base AS runtime +USER ironclaw