Skip to content

Commit 0a2cb93

Browse files
Optimize Docker image size with lean and Python variants (#259)
* fix: exclude src and tsconfig from npm package Add files field to package.json to explicitly list published files. Fixes linting errors for users whose IDEs try to resolve the workspace-only @repo/typescript-config reference in tsconfig.json. * build: bundle sandbox-container with Bun Replace tsc compilation with Bun.build() bundling. This inlines all dependencies (@repo/shared, zod, async-mutex) into single files, eliminating the need for node_modules in the container image. * feat: transpile TypeScript in process-pool using Bun.Transpiler Move TypeScript transpilation from the executor to process-pool.ts. This allows us to use Bun's built-in transpiler (since process-pool runs on Bun) and send plain JavaScript to the Node-based executor. * refactor: consolidate JS and TS to single executor TypeScript is now transpiled in process-pool.ts before execution, so both JS and TS can use the same node_executor. This simplifies the codebase and removes the need for the separate ts_executor. * chore: remove esbuild dependency esbuild is no longer needed since TypeScript transpilation now uses Bun's built-in Bun.Transpiler instead. * build: remove node_modules from Docker image Use bundled output instead of copying node_modules. The container server and JS executor are now self-contained bundles with all dependencies inlined. Reduces image size by ~670MB. * feat: add Python-not-available error for lean image When PYTHON_POOL_MIN_SIZE=0, return a helpful error message directing users to the -python image variant instead of failing cryptically. * feat: add multi-target Dockerfile for image variants - default: lean image without Python (~600-800MB) - python: full image with Python + data science packages (~1.3GB) Users can choose the appropriate variant based on their needs. The default image is suitable for most use cases (shell commands, file operations, JS/TS code interpreter). The python variant adds Python code interpreter with matplotlib, numpy, pandas, ipython. * build: update docker scripts to build both variants docker:local, docker:publish, and docker:publish:beta now build both the default and -python image variants. * fix: proper Python detection and CI updates for image variants - Python check now invokes python3 binary instead of checking env var - E2E test-worker and integration Dockerfiles use -python variant - CI workflows build python target for E2E tests - Release workflow pushes both default and -python variants - Remove unused docker:publish scripts (CI handles publishing) * chore: add changeset for image optimization * build: update pkg-pr-new workflow for image variants Build and push both default and -python Docker image variants. Update PR comment to show both image options. * fix: check Python availability before reserving executor Move Python availability check earlier in the flow so context creation fails with a helpful error message before trying to spawn python3. * test: add E2E test for base image Python error Add E2E test validating the Python-not-available error message when using the base image variant. Updates CI to build both image variants and auto-regenerates wrangler.jsonc from template before E2E tests. * chore: simplify changeset description * Add PYTHON_NOT_AVAILABLE error code with proper HTTP 501 status When Python is unavailable in the base image, the error now returns HTTP 501 with the PYTHON_NOT_AVAILABLE error code instead of a generic 500 INTERNAL_ERROR. The interpreter service detects this specific error and propagates the correct code through the error handling chain. Also fixes an untyped array in ensureMinimumPool and strengthens the changeset breaking change warning for Python users. * Run JS tests on base image and use dynamic version in error message JavaScript-only E2E tests now run against the base image to validate the lean image works correctly for non-Python workloads. This provides coverage for the bundled JS executor without requiring Python. The Python unavailable error message now uses SANDBOX_VERSION env var when available, falling back to <version> placeholder. This gives users the exact version to use in production while keeping the placeholder for development. * Refactor container naming: base image default, Python explicit - Rename SandboxBase → SandboxPython in test worker - Flip header logic: X-Sandbox-Type: python selects Python image - Default (no header) now uses base image (smaller, no Python) - Update cleanup script for -python suffix - Remove per-run CI cleanup (resources persist until PR closes) * Fix Docker comment update to match plural 'Images' * Fix Python unavailable test to expect 500 status
1 parent 458d140 commit 0a2cb93

File tree

24 files changed

+438
-786
lines changed

24 files changed

+438
-786
lines changed

.changeset/image-optimization.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@cloudflare/sandbox': minor
3+
---
4+
5+
Add lean and Python image variants to reduce Docker image size
6+
7+
**BREAKING CHANGE for Python users:** The default image no longer includes Python.
8+
9+
- `cloudflare/sandbox:<version>` - lean image without Python (~600-800MB)
10+
- `cloudflare/sandbox:<version>-python` - full image with Python + data science packages (~1.3GB)
11+
12+
**Migration:** If using `CodeInterpreter.runCode()` with Python, update your Dockerfile:
13+
14+
```dockerfile
15+
# Before
16+
FROM cloudflare/sandbox:0.5.6
17+
18+
# After
19+
FROM cloudflare/sandbox:0.5.6-python
20+
```
21+
22+
Without this change, Python execution will fail with `PYTHON_NOT_AVAILABLE` error.

.github/workflows/pkg-pr-new.yml

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,18 +77,35 @@ jobs:
7777
username: ${{ secrets.DOCKER_HUB_USERNAME }}
7878
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
7979

80-
- name: Build and push Docker image (preview)
80+
- name: Build and push Docker image (default)
8181
uses: docker/build-push-action@v6
8282
with:
8383
context: .
8484
file: packages/sandbox/Dockerfile
85+
target: default
8586
platforms: linux/amd64
8687
push: true
8788
tags: cloudflare/sandbox:${{ steps.package-version.outputs.version }}
8889
cache-from: |
89-
type=gha,scope=preview-pr-${{ github.event.pull_request.number }}
90-
type=gha,scope=release
91-
cache-to: type=gha,mode=max,scope=preview-pr-${{ github.event.pull_request.number }}
90+
type=gha,scope=preview-pr-${{ github.event.pull_request.number }}-default
91+
type=gha,scope=release-default
92+
cache-to: type=gha,mode=max,scope=preview-pr-${{ github.event.pull_request.number }}-default
93+
build-args: |
94+
SANDBOX_VERSION=${{ steps.package-version.outputs.version }}
95+
96+
- name: Build and push Docker image (python)
97+
uses: docker/build-push-action@v6
98+
with:
99+
context: .
100+
file: packages/sandbox/Dockerfile
101+
target: python
102+
platforms: linux/amd64
103+
push: true
104+
tags: cloudflare/sandbox:${{ steps.package-version.outputs.version }}-python
105+
cache-from: |
106+
type=gha,scope=preview-pr-${{ github.event.pull_request.number }}-python
107+
type=gha,scope=release-python
108+
cache-to: type=gha,mode=max,scope=preview-pr-${{ github.event.pull_request.number }}-python
92109
build-args: |
93110
SANDBOX_VERSION=${{ steps.package-version.outputs.version }}
94111
@@ -100,8 +117,9 @@ jobs:
100117
with:
101118
script: |
102119
const version = '${{ steps.package-version.outputs.version }}';
103-
const dockerTag = `cloudflare/sandbox:${version}`;
104-
const body = `### 🐳 Docker Image Published\n\n\`\`\`dockerfile\nFROM ${dockerTag}\n\`\`\`\n\n**Version:** \`${version}\`\n\nYou can use this Docker image with the preview package from this PR.`;
120+
const defaultTag = `cloudflare/sandbox:${version}`;
121+
const pythonTag = `cloudflare/sandbox:${version}-python`;
122+
const body = `### 🐳 Docker Images Published\n\n**Default (no Python):**\n\`\`\`dockerfile\nFROM ${defaultTag}\n\`\`\`\n\n**With Python:**\n\`\`\`dockerfile\nFROM ${pythonTag}\n\`\`\`\n\n**Version:** \`${version}\`\n\nUse the \`-python\` variant if you need Python code execution.`;
105123
106124
// Find existing comment
107125
const { data: comments } = await github.rest.issues.listComments({
@@ -112,7 +130,7 @@ jobs:
112130
113131
const botComment = comments.find(comment =>
114132
comment.user.type === 'Bot' &&
115-
comment.body.includes('Docker Image Published')
133+
comment.body.includes('Docker Images Published')
116134
);
117135
118136
if (botComment) {

.github/workflows/pullrequest.yml

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -116,16 +116,15 @@ jobs:
116116
- name: Set up Docker Buildx
117117
uses: docker/setup-buildx-action@v3
118118

119-
- name: Build test worker Docker image
120-
uses: docker/build-push-action@v6
121-
with:
122-
context: .
123-
file: packages/sandbox/Dockerfile
124-
platforms: linux/amd64 # Explicit single-arch for compatibility with release-amd64 cache
125-
load: true # Load into Docker daemon for local testing
126-
tags: cloudflare/sandbox-test:${{ needs.unit-tests.outputs.version || '0.0.0' }}
127-
build-args: |
128-
SANDBOX_VERSION=${{ needs.unit-tests.outputs.version || '0.0.0' }}
119+
- name: Build test worker Docker images (base + python)
120+
run: |
121+
VERSION=${{ needs.unit-tests.outputs.version || '0.0.0' }}
122+
# Build base image (no Python) - used by SandboxBase binding
123+
docker build -f packages/sandbox/Dockerfile --target default --platform linux/amd64 \
124+
--build-arg SANDBOX_VERSION=$VERSION -t cloudflare/sandbox-test:$VERSION .
125+
# Build python image - used by Sandbox binding
126+
docker build -f packages/sandbox/Dockerfile --target python --platform linux/amd64 \
127+
--build-arg SANDBOX_VERSION=$VERSION -t cloudflare/sandbox-test:$VERSION-python .
129128
130129
# Deploy test worker using official Cloudflare action
131130
- name: Deploy test worker
@@ -161,16 +160,10 @@ jobs:
161160
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
162161
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
163162

164-
# Cleanup: Delete test worker and container (only for PR environments)
165-
- name: Cleanup test deployment
166-
if: always() && github.event_name == 'pull_request'
167-
continue-on-error: true
168-
run: |
169-
cd tests/e2e/test-worker
170-
../../../scripts/cleanup-test-deployment.sh ${{ steps.env-name.outputs.worker_name }}
171-
env:
172-
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
173-
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
163+
# Note: Resources are NOT cleaned up after each run to speed up subsequent CI runs.
164+
# Cleanup happens via:
165+
# - cleanup.yml: Triggered when PR is closed
166+
# - cleanup-stale.yml: Daily cron job for orphaned/stale resources
174167

175168
# Validate changesets don't contain internal packages
176169
validate-changesets:

.github/workflows/release.yml

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,10 @@ jobs:
109109
with:
110110
context: .
111111
file: packages/sandbox/Dockerfile
112+
target: python # E2E tests require Python for code interpreter tests
112113
platforms: linux/amd64
113114
load: true
114-
tags: cloudflare/sandbox-test:${{ needs.unit-tests.outputs.version }}
115+
tags: cloudflare/sandbox-test:${{ needs.unit-tests.outputs.version }}-python
115116
build-args: |
116117
SANDBOX_VERSION=${{ needs.unit-tests.outputs.version }}
117118
@@ -177,16 +178,31 @@ jobs:
177178
username: ${{ secrets.DOCKER_HUB_USERNAME }}
178179
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
179180

180-
- name: Build and push Docker image
181+
- name: Build and push Docker image (default)
181182
uses: docker/build-push-action@v6
182183
with:
183184
context: .
184185
file: packages/sandbox/Dockerfile
186+
target: default
185187
platforms: linux/amd64
186188
push: true
187189
tags: cloudflare/sandbox:${{ needs.unit-tests.outputs.version }}
188-
cache-from: type=gha,scope=release
189-
cache-to: type=gha,mode=max,scope=release
190+
cache-from: type=gha,scope=release-default
191+
cache-to: type=gha,mode=max,scope=release-default
192+
build-args: |
193+
SANDBOX_VERSION=${{ needs.unit-tests.outputs.version }}
194+
195+
- name: Build and push Docker image (python)
196+
uses: docker/build-push-action@v6
197+
with:
198+
context: .
199+
file: packages/sandbox/Dockerfile
200+
target: python
201+
platforms: linux/amd64
202+
push: true
203+
tags: cloudflare/sandbox:${{ needs.unit-tests.outputs.version }}-python
204+
cache-from: type=gha,scope=release-python
205+
cache-to: type=gha,mode=max,scope=release-python
190206
build-args: |
191207
SANDBOX_VERSION=${{ needs.unit-tests.outputs.version }}
192208

0 commit comments

Comments
 (0)