diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 5286993c..7a5d267e 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -1,7 +1,7 @@ name: Claude Code Review on: - pull_request_target: + pull_request: types: [opened, synchronize] # Optional: Only run on specific file changes # paths: @@ -13,12 +13,9 @@ on: jobs: claude-review: # Skip review for automated "Version Packages" PRs created by changesets - # For external PRs: requires manual approval via 'external-pr' environment - # For internal PRs: runs automatically without approval if: github.event.pull_request.title != 'Version Packages' runs-on: ubuntu-latest - environment: ${{ github.event.pull_request.head.repo.full_name != github.repository && 'external-pr' || null }} permissions: contents: read pull-requests: read @@ -29,7 +26,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 1 - name: Run Claude Code Review diff --git a/.github/workflows/pkg-pr-new.yml b/.github/workflows/pkg-pr-new.yml index ffabbad4..759a56e6 100644 --- a/.github/workflows/pkg-pr-new.yml +++ b/.github/workflows/pkg-pr-new.yml @@ -5,7 +5,7 @@ permissions: pull-requests: write # Required for pkg.pr.new to comment on PRs on: - pull_request_target: + pull_request: types: [opened, synchronize, reopened] paths: - '**' @@ -14,17 +14,13 @@ on: jobs: publish-preview: - # For external PRs: requires manual approval via 'external-pr' environment - # For internal PRs: runs automatically without approval runs-on: ubuntu-latest timeout-minutes: 15 - environment: ${{ github.event.pull_request.head.repo.full_name != github.repository && 'external-pr' || null }} steps: - name: Checkout code uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - name: Setup Node.js diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index b885cad0..3138cb55 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -4,8 +4,7 @@ permissions: contents: read on: - pull_request_target: - types: [opened, synchronize, reopened] + pull_request: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -20,8 +19,6 @@ jobs: version: ${{ steps.get-version.outputs.version }} steps: - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - uses: actions/setup-node@v4 with: @@ -74,17 +71,12 @@ jobs: run: npm run test -w @repo/sandbox-container # E2E tests against deployed worker - # For external PRs: requires manual approval via 'external-pr' environment - # For internal PRs: runs automatically without approval e2e-tests: needs: unit-tests timeout-minutes: 30 runs-on: ubuntu-latest - environment: ${{ github.event.pull_request.head.repo.full_name != github.repository && 'external-pr' || null }} steps: - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - uses: actions/setup-node@v4 with: @@ -105,7 +97,7 @@ jobs: - name: Set environment name id: env-name run: | - if [ "${{ github.event_name }}" = "pull_request_target" ]; then + if [ "${{ github.event_name }}" = "pull_request" ]; then echo "env_name=pr-${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT echo "worker_name=sandbox-e2e-test-worker-pr-${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT else @@ -163,7 +155,7 @@ jobs: # Cleanup: Delete test worker and container (only for PR environments) - name: Cleanup test deployment - if: always() && github.event_name == 'pull_request_target' + if: always() && github.event_name == 'pull_request' continue-on-error: true run: | cd tests/e2e/test-worker diff --git a/CLAUDE.md b/CLAUDE.md index c6bc4a85..95ada72b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -348,7 +348,7 @@ different users share the same sandbox instance. The container runtime uses Ubuntu 22.04 with: - Python 3.11 (with matplotlib, numpy, pandas, ipython) -- Node.js 20 LTS +- Node.js 24 LTS - Bun 1.x runtime (powers the container HTTP server) - Git, curl, wget, jq, and other common utilities diff --git a/docs/SESSION_EXECUTION.md b/docs/SESSION_EXECUTION.md index 3a4d44f6..06bdeb22 100644 --- a/docs/SESSION_EXECUTION.md +++ b/docs/SESSION_EXECUTION.md @@ -14,22 +14,20 @@ This document explains how the container session executes commands reliably whil ### Foreground (`exec`) - Runs in the main bash shell so state persists across commands. -- Uses bash process substitution to prefix stdout/stderr inline and append to the per-command log file. -- After the command returns, we `wait` to ensure process-substitution consumers finish writing to the log before we write the exit code file. -- Why process substitution (not FIFOs): - - Foreground previously used FIFOs + background labelers, which can race on silent commands because FIFO open/close ordering depends on cross-process scheduling. - - Process substitution keeps execution local to the main shell and avoids FIFO semantics entirely. +- Writes stdout/stderr to temporary files, then prefixes and merges them into the log. +- Bash waits for file redirects to complete before continuing, ensuring the log is fully written before the exit code is published. +- This avoids race conditions from process substitution buffering where log reads could happen before writes complete. Pseudo: ``` # Foreground -{ command; } \ - > >(while read; printf "\x01\x01\x01%s\n" "$REPLY" >> "$log") \ - 2> >(while read; printf "\x02\x02\x02%s\n" "$REPLY" >> "$log") +{ command; } > "$log.stdout" 2> "$log.stderr" EXIT_CODE=$? -# Ensure consumers have drained -wait 2>/dev/null +# Prefix and merge into main log +(while read line; do printf "\x01\x01\x01%s\n" "$line"; done < "$log.stdout") >> "$log" +(while read line; do printf "\x02\x02\x02%s\n" "$line"; done < "$log.stderr") >> "$log" +rm -f "$log.stdout" "$log.stderr" # Atomically publish exit code echo "$EXIT_CODE" > "$exit.tmp" && mv "$exit.tmp" "$exit" ``` @@ -97,3 +95,5 @@ mkfifo "$sp" "$ep" - Why not tee? Tee doesn’t split stdout/stderr into separate channels with stable ordering without extra plumbing; our prefixes are simple and explicit. - Is process substitution portable? - It is supported by bash (we spawn bash with `--norc`). The container environment supports it; if portability constraints change, we can revisit. +- Why use temp files instead of process substitution for foreground? + Process substitutions run asynchronously - bash returns when the substitution processes close, but their writes to the log file may still be buffered. With large output (e.g., base64-encoded files), the log file can be incomplete when we try to read it. Using direct file redirects ensures bash waits for all writes to complete before continuing, eliminating this race condition. diff --git a/examples/claude-code/Dockerfile b/examples/claude-code/Dockerfile index 15636dc7..a67d1d4e 100644 --- a/examples/claude-code/Dockerfile +++ b/examples/claude-code/Dockerfile @@ -1,7 +1,7 @@ -FROM docker.io/cloudflare/sandbox:0.4.14 +FROM docker.io/cloudflare/sandbox:0.4.15 RUN npm install -g @anthropic-ai/claude-code ENV COMMAND_TIMEOUT_MS=300000 EXPOSE 3000 # On a Mac with Apple Silicon, you might need to specify the platform: -# FROM --platform=linux/arm64 docker.io/cloudflare/sandbox:0.4.14 +# FROM --platform=linux/arm64 docker.io/cloudflare/sandbox:0.4.15 diff --git a/examples/code-interpreter/Dockerfile b/examples/code-interpreter/Dockerfile index 9e857905..7ff56059 100644 --- a/examples/code-interpreter/Dockerfile +++ b/examples/code-interpreter/Dockerfile @@ -1,9 +1,9 @@ # This image is unique to this repo, and you'll never need it. # Whenever you're integrating with sandbox SDK in your own project, # you should use the official image instead: -# FROM docker.io/cloudflare/sandbox:0.4.14 -FROM cloudflare/sandbox-test:0.4.14 +# FROM docker.io/cloudflare/sandbox:0.4.15 +FROM cloudflare/sandbox-test:0.4.15 # On a mac, you might need to actively pick up the # arm64 build of the image. -# FROM --platform=linux/arm64 cloudflare/sandbox-test:0.4.14 +# FROM --platform=linux/arm64 cloudflare/sandbox-test:0.4.15 diff --git a/examples/minimal/Dockerfile b/examples/minimal/Dockerfile index cbcc78dc..981a5dd9 100644 --- a/examples/minimal/Dockerfile +++ b/examples/minimal/Dockerfile @@ -1,7 +1,7 @@ -FROM docker.io/cloudflare/sandbox:0.4.14 +FROM docker.io/cloudflare/sandbox:0.4.15 # On a Mac with Apple Silicon, you might need to specify the platform: -# FROM --platform=linux/arm64 docker.io/cloudflare/sandbox:0.4.14 +# FROM --platform=linux/arm64 docker.io/cloudflare/sandbox:0.4.15 # Required during local development to access exposed ports EXPOSE 8080 diff --git a/package-lock.json b/package-lock.json index 0f03c891..f22eca25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8823,7 +8823,7 @@ }, "packages/sandbox": { "name": "@cloudflare/sandbox", - "version": "0.4.14", + "version": "0.4.15", "license": "ISC", "dependencies": { "@cloudflare/containers": "^0.0.30" diff --git a/packages/sandbox-container/src/session.ts b/packages/sandbox-container/src/session.ts index 2406c570..ed9dfe26 100644 --- a/packages/sandbox-container/src/session.ts +++ b/packages/sandbox-container/src/session.ts @@ -722,17 +722,17 @@ export class Session { // FOREGROUND PATTERN (for exec) // Command runs in main shell, state persists! - // FOREGROUND: Avoid FIFOs to eliminate race conditions. - // Use bash process substitution to prefix stdout/stderr while keeping - // execution in the main shell so session state persists across commands. + // FOREGROUND: Write stdout/stderr to temp files, then prefix and merge. + // This ensures bash waits for all writes to complete before continuing, + // avoiding race conditions when reading the log file. if (cwd) { const safeCwd = this.escapeShellPath(cwd); script += ` # Save and change directory\n`; script += ` PREV_DIR=$(pwd)\n`; script += ` if cd ${safeCwd}; then\n`; - script += ` # Execute command with prefixed streaming via process substitution\n`; - script += ` { ${command}; } < /dev/null > >(while IFS= read -r line || [[ -n "$line" ]]; do printf '\\x01\\x01\\x01%s\\n' "$line"; done >> "$log") 2> >(while IFS= read -r line || [[ -n "$line" ]]; do printf '\\x02\\x02\\x02%s\\n' "$line"; done >> "$log")\n`; + script += ` # Execute command, redirect to temp files\n`; + script += ` { ${command}; } < /dev/null > "$log.stdout" 2> "$log.stderr"\n`; script += ` EXIT_CODE=$?\n`; script += ` # Restore directory\n`; script += ` cd "$PREV_DIR"\n`; @@ -741,13 +741,16 @@ export class Session { script += ` EXIT_CODE=1\n`; script += ` fi\n`; } else { - script += ` # Execute command with prefixed streaming via process substitution\n`; - script += ` { ${command}; } < /dev/null > >(while IFS= read -r line || [[ -n "$line" ]]; do printf '\\x01\\x01\\x01%s\\n' "$line"; done >> "$log") 2> >(while IFS= read -r line || [[ -n "$line" ]]; do printf '\\x02\\x02\\x02%s\\n' "$line"; done >> "$log")\n`; + script += ` # Execute command, redirect to temp files\n`; + script += ` { ${command}; } < /dev/null > "$log.stdout" 2> "$log.stderr"\n`; script += ` EXIT_CODE=$?\n`; } - // Ensure process-substitution consumers complete before writing exit code - script += ` wait 2>/dev/null\n`; + script += ` \n`; + script += ` # Prefix and merge stdout/stderr into main log\n`; + script += ` (while IFS= read -r line || [[ -n "$line" ]]; do printf '\\x01\\x01\\x01%s\\n' "$line"; done < "$log.stdout" >> "$log") 2>/dev/null\n`; + script += ` (while IFS= read -r line || [[ -n "$line" ]]; do printf '\\x02\\x02\\x02%s\\n' "$line"; done < "$log.stderr" >> "$log") 2>/dev/null\n`; + script += ` rm -f "$log.stdout" "$log.stderr"\n`; script += ` \n`; script += ` # Write exit code\n`; script += ` echo "$EXIT_CODE" > ${safeExitCodeFile}.tmp\n`; diff --git a/packages/sandbox/CHANGELOG.md b/packages/sandbox/CHANGELOG.md index bc847e9a..8ad3f909 100644 --- a/packages/sandbox/CHANGELOG.md +++ b/packages/sandbox/CHANGELOG.md @@ -1,5 +1,13 @@ # @cloudflare/sandbox +## 0.4.15 + +### Patch Changes + +- [#185](https://github.com/cloudflare/sandbox-sdk/pull/185) [`7897cdd`](https://github.com/cloudflare/sandbox-sdk/commit/7897cddefc366bbd640ea138b34a520a0b2ddf8c) Thanks [@ghostwriternr](https://github.com/ghostwriternr)! - Fix foreground commands blocking on background processes + +- [#183](https://github.com/cloudflare/sandbox-sdk/pull/183) [`ff2fa91`](https://github.com/cloudflare/sandbox-sdk/commit/ff2fa91479357ef88cfb22418f88acb257462faa) Thanks [@whoiskatrin](https://github.com/whoiskatrin)! - update python to 3.11.14 + ## 0.4.14 ### Patch Changes diff --git a/packages/sandbox/Dockerfile b/packages/sandbox/Dockerfile index d445cfb3..6a9a6382 100644 --- a/packages/sandbox/Dockerfile +++ b/packages/sandbox/Dockerfile @@ -4,7 +4,7 @@ # ============================================================================ # Stage 1: Prune monorepo to only include necessary packages # ============================================================================ -FROM node:20-alpine AS pruner +FROM node:lts-alpine AS pruner WORKDIR /app @@ -21,7 +21,7 @@ RUN turbo prune @repo/sandbox-container --docker # ============================================================================ # Stage 2: Install dependencies and build packages # ============================================================================ -FROM node:20-alpine AS builder +FROM node:lts-alpine AS builder WORKDIR /app @@ -43,7 +43,7 @@ RUN npx turbo run build # ============================================================================ # Stage 3: Install production-only dependencies # ============================================================================ -FROM node:20-alpine AS prod-deps +FROM node:lts-alpine AS prod-deps WORKDIR /app @@ -58,7 +58,49 @@ RUN --mount=type=cache,target=/root/.npm \ npm ci --production # ============================================================================ -# Stage 4: Runtime - Ubuntu 22.04 with only runtime dependencies +# Stage 4: Download pre-built Python 3.11.14 +# ============================================================================ +FROM ubuntu:22.04 AS python-builder + +# Prevent interactive prompts during package installation +ENV DEBIAN_FRONTEND=noninteractive + +# Accept architecture from Docker BuildKit (for multi-arch builds) +ARG TARGETARCH + +# Install minimal dependencies for downloading +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + rm -f /etc/apt/apt.conf.d/docker-clean && \ + echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache && \ + apt-get update && apt-get install -y --no-install-recommends \ + wget ca-certificates + +# Download and extract pre-built Python 3.11.14 from python-build-standalone +# Using PGO+LTO optimized builds for better performance +# Supports multi-arch: amd64 (x86_64) and arm64 (aarch64) +RUN --mount=type=cache,target=/tmp/python-cache \ + # Map Docker TARGETARCH to python-build-standalone arch naming + if [ "$TARGETARCH" = "amd64" ]; then \ + PYTHON_ARCH="x86_64-unknown-linux-gnu"; \ + EXPECTED_SHA256="edd8d11aa538953d12822fab418359a692fd1ee4ca2675579fbf0fa31e3688f1"; \ + elif [ "$TARGETARCH" = "arm64" ]; then \ + PYTHON_ARCH="aarch64-unknown-linux-gnu"; \ + EXPECTED_SHA256="08141d31f95d86a23f23e4c741b726de0055f12f83200d1d4867b4e8e6e967c5"; \ + else \ + echo "Unsupported architecture: $TARGETARCH" && exit 1; \ + fi && \ + cd /tmp/python-cache && \ + wget -nc https://github.com/indygreg/python-build-standalone/releases/download/20251028/cpython-3.11.14+20251028-${PYTHON_ARCH}-install_only.tar.gz && \ + # Verify SHA256 checksum for security + echo "${EXPECTED_SHA256} cpython-3.11.14+20251028-${PYTHON_ARCH}-install_only.tar.gz" | sha256sum -c - && \ + cd /tmp && \ + tar -xzf /tmp/python-cache/cpython-3.11.14+20251028-${PYTHON_ARCH}-install_only.tar.gz && \ + mv python /usr/local/ && \ + rm -rf /tmp/cpython-* + +# ============================================================================ +# Stage 5: Runtime - Ubuntu 22.04 with only runtime dependencies # ============================================================================ FROM ubuntu:22.04 AS runtime @@ -71,44 +113,43 @@ ENV DEBIAN_FRONTEND=noninteractive # Set the sandbox version as an environment variable for version checking ENV SANDBOX_VERSION=${SANDBOX_VERSION} -# Install essential runtime packages with cache mounts +# Install runtime packages and Python runtime libraries RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ rm -f /etc/apt/apt.conf.d/docker-clean && \ echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache && \ apt-get update && apt-get install -y --no-install-recommends \ - curl \ - wget \ - ca-certificates \ - python3.11 \ - python3-pip \ - python3.11-venv \ - procps \ - git \ - unzip \ - zip \ - jq \ - file + ca-certificates curl wget procps git unzip zip jq file \ + libssl3 zlib1g libbz2-1.0 libreadline8 libsqlite3-0 \ + libncursesw6 libtinfo6 libxml2 libxmlsec1 libffi8 liblzma5 libtk8.6 && \ + update-ca-certificates + +# Copy pre-built Python from python-builder stage +COPY --from=python-builder /usr/local/python /usr/local/python + +# Create symlinks and update shared library cache +RUN ln -s /usr/local/python/bin/python3.11 /usr/local/bin/python3.11 && \ + ln -s /usr/local/python/bin/python3 /usr/local/bin/python3 && \ + ln -s /usr/local/python/bin/pip3 /usr/local/bin/pip3 && \ + echo "/usr/local/python/lib" > /etc/ld.so.conf.d/python.conf && \ + ldconfig # Set Python 3.11 as default python3 -RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 +RUN update-alternatives --install /usr/bin/python3 python3 /usr/local/bin/python3.11 1 -# Install Node.js 20 LTS using official NodeSource setup script -RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ - && apt-get install -y nodejs \ - && rm -rf /var/lib/apt/lists/* +# Install Python packages +RUN --mount=type=cache,target=/root/.cache/pip \ + pip3 install --no-cache-dir matplotlib numpy pandas ipython + +# Install Node.js 24 LTS from official Node image +COPY --from=node:24-slim /usr/local/bin/node /usr/local/bin/node +COPY --from=node:24-slim /usr/local/lib/node_modules /usr/local/lib/node_modules +RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \ + ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx # Install Bun runtime from official image COPY --from=oven/bun:1 /usr/local/bin/bun /usr/local/bin/bun -# Install essential Python packages with cache mount -RUN --mount=type=cache,target=/root/.cache/pip \ - pip3 install \ - matplotlib \ - numpy \ - pandas \ - ipython - # Set up runtime container server directory WORKDIR /container-server diff --git a/packages/sandbox/package.json b/packages/sandbox/package.json index 9d5161b0..bc0bc233 100644 --- a/packages/sandbox/package.json +++ b/packages/sandbox/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/sandbox", - "version": "0.4.14", + "version": "0.4.15", "repository": { "type": "git", "url": "https://github.com/cloudflare/sandbox-sdk" diff --git a/packages/sandbox/src/version.ts b/packages/sandbox/src/version.ts index 1115aa76..d8ec91d5 100644 --- a/packages/sandbox/src/version.ts +++ b/packages/sandbox/src/version.ts @@ -3,4 +3,4 @@ * This file is auto-updated by .github/changeset-version.ts during releases * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump */ -export const SDK_VERSION = '0.4.14'; +export const SDK_VERSION = '0.4.15'; diff --git a/tests/e2e/process-lifecycle-workflow.test.ts b/tests/e2e/process-lifecycle-workflow.test.ts index ef6730e5..d15f2cf4 100644 --- a/tests/e2e/process-lifecycle-workflow.test.ts +++ b/tests/e2e/process-lifecycle-workflow.test.ts @@ -186,6 +186,61 @@ describe('Process Lifecycle Workflow', () => { }); }, 90000); + test('should not block foreground operations when background processes are running', async () => { + const sandboxId = createSandboxId(); + const headers = createTestHeaders(sandboxId); + + // Start a long-running background process + const startResponse = await vi.waitFor( + async () => + fetchWithStartup(`${workerUrl}/api/process/start`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: 'sleep 60' + }) + }), + { timeout: 90000, interval: 2000 } + ); + + const startData = await startResponse.json(); + const processId = startData.id; + + // Immediately run a foreground command - should complete quickly + const execStart = Date.now(); + const execResponse = await fetch(`${workerUrl}/api/execute`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: 'echo "test"' + }) + }); + const execDuration = Date.now() - execStart; + + expect(execResponse.status).toBe(200); + expect(execDuration).toBeLessThan(2000); // Should complete in <2s, not wait for sleep + + // Test listFiles as well - it uses the same foreground execution path + const listStart = Date.now(); + const listResponse = await fetch(`${workerUrl}/api/list-files`, { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/workspace' + }) + }); + const listDuration = Date.now() - listStart; + + expect(listResponse.status).toBe(200); + expect(listDuration).toBeLessThan(2000); // Should complete quickly + + // Cleanup + await fetch(`${workerUrl}/api/process/${processId}`, { + method: 'DELETE', + headers + }); + }, 90000); + test('should get process logs after execution', async () => { const sandboxId = createSandboxId(); const headers = createTestHeaders(sandboxId); diff --git a/tests/e2e/test-worker/Dockerfile b/tests/e2e/test-worker/Dockerfile index 0b599b60..e4b57aab 100644 --- a/tests/e2e/test-worker/Dockerfile +++ b/tests/e2e/test-worker/Dockerfile @@ -1,5 +1,5 @@ # Integration test Dockerfile -FROM docker.io/cloudflare/sandbox-test:0.4.14 +FROM docker.io/cloudflare/sandbox-test:0.4.15 # Expose ports used for testing EXPOSE 8080 diff --git a/tests/integration/Dockerfile b/tests/integration/Dockerfile index f38075f9..18090622 100644 --- a/tests/integration/Dockerfile +++ b/tests/integration/Dockerfile @@ -1,12 +1,12 @@ # This image is unique to this repo, and you'll never need it. # Whenever you're integrating with sandbox SDK in your own project, # you should use the official image instead: -# FROM docker.io/cloudflare/sandbox:0.4.14 -FROM cloudflare/sandbox-test:0.4.14 +# FROM docker.io/cloudflare/sandbox:0.4.15 +FROM cloudflare/sandbox-test:0.4.15 # On a mac, you might need to actively pick up the # arm64 build of the image. -# FROM --platform=linux/arm64 cloudflare/sandbox-test:0.4.14 +# FROM --platform=linux/arm64 cloudflare/sandbox-test:0.4.15 # Expose the ports you want to expose EXPOSE 8080