Experimental: This is an experimental project and not an official DigitalOcean product.
Python SDK providing sandbox capabilities for DigitalOcean App Platform.
Sandbox/AsyncSandbox (API) → Executor (pexpect) → doctl apps console → Container
SandboxManager (Pool) → AsyncSandbox → Executor → Container
src/do_app_sandbox/sandbox.py- Main Sandbox class (sync API)src/do_app_sandbox/async_sandbox.py- AsyncSandbox class (async API)src/do_app_sandbox/manager.py- SandboxManager for pool managementsrc/do_app_sandbox/adaptive_pool.py- Scaling algorithms (EMA, PID, Predictive)src/do_app_sandbox/executor.py- Command execution via pexpectsrc/do_app_sandbox/filesystem.py- File operations (base64)src/do_app_sandbox/process.py- Process managementsrc/do_app_sandbox/deployer.py- App Platform deploymentsrc/do_app_sandbox/cli.py- CLI commandssrc/do_app_sandbox/image_registry.py- Custom image managementsrc/do_app_sandbox/image_validator.py- Image validationsrc/do_app_sandbox/spaces.py- DO Spaces integration for large filessrc/do_app_sandbox/types.py- Type definitionssrc/do_app_sandbox/exceptions.py- Custom exceptions
sandbox create --image python|node # Create sandbox (uses GHCR public images by default)
sandbox exec SANDBOX "command" # Execute command
sandbox list / sandbox delete # Manage sandboxesDIGITALOCEAN_TOKEN # Used for doctl auth (optional if already authenticated)
GHCR_OWNER # GHCR namespace/owner (default: bikramkgupta)
GHCR_REGISTRY # GHCR host (default: ghcr.io)
APP_SANDBOX_REGION # Default region (atl1)
# For large file transfers via Spaces
SPACES_ACCESS_KEY # DO Spaces access key
SPACES_SECRET_KEY # DO Spaces secret key
SPACES_BUCKET # Default bucket name
SPACES_REGION # Default region (e.g., nyc3)
SPACES_ENDPOINT # Optional custom endpoint (will be normalized if bucket-prefixed)
Use doctl directly:
doctl apps logs -f APP_ID COMPONENT --type run # Follow runtime logs
doctl apps logs -f APP_ID COMPONENT --type build # Follow build logs
doctl apps logs -f APP_ID COMPONENT --type deploy # Follow deploy logsimages/python/- Ubuntu 24.04 + Python 3.13 + uvimages/node/- Ubuntu 24.04 + Node.js 24 + nvm- Both images include: curl, git, lsof, jq, procps
- Working directory:
/home/sandbox/app(with/appas symlink)
- Port 8080: Completely free for user applications
- Port 9090: Internal health server (handled by sandbox, not user)
from do_app_sandbox import Sandbox
sandbox = Sandbox.create(image="python")
result = sandbox.exec("python --version")
print(result.stdout)
sandbox.delete()
# Context manager
with Sandbox.create(image="python") as sandbox:
result = sandbox.exec("echo 'Hello'")from do_app_sandbox import AsyncSandbox
sandbox = await AsyncSandbox.create(image="python")
result = await sandbox.exec("python --version")
await sandbox.delete()Eliminates cold-start latency by maintaining pre-warmed sandboxes:
from do_app_sandbox import SandboxManager, PoolConfig
manager = SandboxManager(
pools={"python": PoolConfig(target_ready=3, max_ready=10)},
)
await manager.start()
# Instant acquisition from pool
sandbox = await manager.acquire(image="python")
result = sandbox.exec("python --version")
sandbox.delete()
await manager.shutdown()PoolConfig options:
target_ready- Target number of ready sandboxes (default: 0)max_ready- Maximum sandboxes in pool (default: 10)idle_timeout- Seconds before scaling down (default: 60)max_warm_age- Max seconds a sandbox can warm (default: 1800)
# Works with any App Platform app
sandbox = Sandbox.get_from_id(app_id, component="your-component")- Service (default): Has public HTTP endpoint on port 8080
- Worker: No HTTP endpoint, for background tasks
sandbox = Sandbox.create(image="python", component_type="worker")sandbox = Sandbox.create(image="python", spaces_config=spaces_config)
sandbox.exec("pip install flask")
# Create full snapshot (captures entire workspace)
meta = sandbox.create_snapshot(description="After installing deps")
# Restore to another sandbox
new_sandbox = Sandbox.create(image="python", spaces_config=spaces_config)
new_sandbox.restore_snapshot(meta.snapshot_id)Capture only files changed since the last snapshot. Reduces upload size by ~95%.
# 1. Full base snapshot
base = sandbox.create_snapshot(description="Base with deps")
# 2. Edit files, then take incremental (only changed files uploaded)
sandbox.exec("echo 'v2' > /home/sandbox/app/version.txt")
incr1 = sandbox.create_incremental_snapshot(
parent_snapshot_id=base.snapshot_id,
description="Version bump",
)
# incr1.size_bytes is ~50 bytes vs base's ~250MB
# 3. More edits + deletions → another incremental
sandbox.exec("rm /home/sandbox/app/old_file.py")
sandbox.exec("echo 'new' > /home/sandbox/app/new_file.py")
incr2 = sandbox.create_incremental_snapshot(
parent_snapshot_id=incr1.snapshot_id,
description="Cleanup + new file",
)
# 4. Restore full chain to a new sandbox (base → incr1 → incr2)
new_sandbox = Sandbox.create(image="python", spaces_config=spaces_config)
new_sandbox.restore_snapshot_chain(incr2.snapshot_id)
# All edits applied, deletions processed, full state restoredKey behaviors:
create_incremental_snapshot()usesfind -newermarker files for change detection- Manifest comparison tracks file deletions between snapshots
- Falls back to full snapshot if container was recycled (marker file lost)
restore_snapshot_chain()resolves the chain automatically and applies layers in order
hibernated = sandbox.hibernate() # snapshot + delete (~$0.02/mo storage)
sandbox = Sandbox.wake(hibernated, spaces_config=spaces_config) # create + restoreFiles >= 5MB use DO Spaces as intermediary:
sandbox = Sandbox.create(
image="python",
spaces_config={
"bucket": "my-bucket",
"region": "nyc3",
"access_key": "...",
"secret_key": "...",
}
)
sandbox.filesystem.upload_large("/local/big.zip", "/app/big.zip")
sandbox.filesystem.download_large("/app/output.tar.gz", "/local/output.tar.gz")The sandbox automatically handles App Platform health checks on port 9090. Your app runs on port 8080 without implementing any health endpoint.
sandbox.filesystem.upload_file("app.py", "/home/sandbox/app/app.py")
# Python requires venv
sandbox.exec("cd /home/sandbox/app && uv venv .venv")
sandbox.exec("cd /home/sandbox/app && source .venv/bin/activate && uv pip install -r requirements.txt")
# Start app
pid = sandbox.launch_process(
"cd /home/sandbox/app && source .venv/bin/activate && python app.py",
cwd="/home/sandbox/app"
)For many files, use zip:
import shutil
shutil.make_archive("/tmp/app", "zip", "/path/to/project")
sandbox.filesystem.upload_file("/tmp/app.zip", "/home/sandbox/app.zip")
sandbox.exec("cd /home/sandbox && unzip -o app.zip -d app && rm app.zip")Key types exported from do_app_sandbox:
CommandResult- Result of exec() with stdout, stderr, exit_codeProcessInfo- Process information (pid, command, status)FileInfo- File metadataAppInfo- App Platform app informationSpacesConfig- Spaces configuration dictPoolConfig- Pool configuration for SandboxManagerPoolMetrics- Pool metrics (ready, in_use, etc.)SnapshotMetadata- Snapshot info (snapshot_id, snapshot_type, chain_depth, parent_snapshot_id, etc.)
from do_app_sandbox import (
SandboxError, # Base exception
SandboxCreationError, # Failed to create sandbox
SandboxNotFoundError, # Sandbox not found
SandboxNotReadyError, # Sandbox not ready
CommandExecutionError, # Command failed
CommandTimeoutError, # Command timed out
FileOperationError, # File operation failed
ConnectionError, # Connection failed
SpacesNotConfiguredError,
ImageNotValidatedError,
ImageValidationError,
PoolError, # Base pool exception
PoolExhaustedError, # No sandboxes available
PoolShutdownError, # Pool is shutting down
WarmUpTimeoutError, # Warm-up timed out
SnapshotChainError, # Broken/cyclic snapshot chain
)Custom Dockerfiles must:
EXPOSE 8080- For user application- Have ENTRYPOINT or CMD
- Optionally: Include the sandbox health server on port 9090
Service Mode Note: If using SandboxMode.SERVICE (HTTP API mode) with a custom image, your Dockerfile must include the sandbox_api FastAPI server. The easiest approach is to base your image on one of the provided service images:
FROM ghcr.io/bikramkgupta/sandbox-python-service:latest
# Add your customizations hereThe exec_stream API endpoint returns output as JSON over Server-Sent Events (SSE). It is designed for text output only. Binary data (non-UTF8) will be decoded with errors='replace', which may corrupt binary content. For binary file transfers, use the Filesystem API (upload_file/download_file) or Spaces for large files.
When using acquire_with_snapshot to restore sandbox state, the snapshot extracts to /workspace by default. The sandbox_api server lives in /opt/sandbox_api. Do not include /opt/sandbox_api in snapshots as overwriting it could destabilize the sandbox. Keep application code and data in /workspace or /home/sandbox/app.
pytest tests/ # Requires DIGITALOCEAN_TOKENFor comprehensive App Platform skills (troubleshooting, deployment, postgres, networking, etc.), see: https://github.com/bikramkgupta/do-app-platform-skills