Morph Cloud lets you spin up remote dev environments (“runtimes”) and control them from Python or the morphcloud CLI.
Use it to:
- Start template-based Devboxes (the quickest way to get a working box)
- Create snapshots and boot instances from them
- Run commands over SSH and copy files in/out
- Expose ports as public URLs (with optional auth)
- Run Docker containers inside instances
- Build snapshot chains when you want caching/reproducibility
For the full docs, start here: Morph Cloud Documentation
- Go to https://cloud.morph.so/web/keys
- Log in with your credentials
- Create a new API key
- Python 3.10 or higher
- An account on Morph Cloud
uv is a fast, modern Python package installer and resolver that works great with Morph Cloud.
# On macOS and Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# On Windows
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"# Create a new project directory
mkdir my-morph-project
cd my-morph-project
# Create a virtual environment with uv
uv venv
# Activate the environment
# On macOS/Linux:
source .venv/bin/activate
# On Windows (cmd):
.venv\Scripts\activate
# On Windows (PowerShell):
.\.venv\Scripts\Activate.ps1
# Now you're ready to install packages like `morphcloud`# Using uv (recommended)
uv pip install morphcloud --upgrade
# Or using traditional pip
pip install morphcloud --upgradeDevboxes are Morph’s batteries-included remote development environments. Start from a template and you get a ready-to-use machine in seconds: SSH in, run your app, expose a port as a URL, and (optionally) save your work.
They’re great for interactive dev/debugging, demos, and “can you repro this?” collaboration—without first building snapshot chains.
This flow starts a devbox, runs a tiny web server inside it, and exposes it as a public URL you can paste into Slack.
# 1) Pick a template
morphcloud devbox template list
# 2) Start a devbox (instant start). Tip: --json writes clean JSON to stdout.
morphcloud devbox start <template-id> --name "hello-devbox" --json > devbox.json
export DEVBOX_ID="$(python3 -c 'import json; print(json.load(open("devbox.json"))["id"])')"
# 3) Run a web server inside the devbox (background)
morphcloud devbox ssh "$DEVBOX_ID" -- bash -lc \
'nohup python3 -m http.server 8000 --bind 0.0.0.0 >/tmp/http.log 2>&1 &'
# 4) Expose port 8000 as a public URL
morphcloud devbox expose-http "$DEVBOX_ID" --name hello --port 8000
# 5) Optional: save your work, then clean up
morphcloud devbox save "$DEVBOX_ID" "hello-webserver"
morphcloud devbox delete "$DEVBOX_ID"Python API:
from morphcloud.api import MorphCloudClient
client = MorphCloudClient()
templates = client.devbox.templates.list_templates()
template_id = templates.data[0].id # pick a template you can access
devbox = client.devbox.start(template_id=template_id, name="hello-devbox")
print(devbox.id)The package ships a CLI too.
# Display version
morphcloud --version
# Get help
morphcloud --helpDevboxes are managed, template-based environments (see the Devboxes section above for a quick walkthrough).
# Templates
morphcloud devbox template list [--json]
morphcloud devbox template get <template-id> [--json]
# Start + connect
morphcloud devbox start <template-id> [--name <name>] [--metadata KEY=VALUE] [--json]
morphcloud devbox ssh <devbox-id> [command...]
# Share a local port as a URL
morphcloud devbox expose-http <devbox-id> --name <name> --port <port> [--auth-mode none|api_key] [--json]
morphcloud devbox hide-http <devbox-id> <name> [--json]
# Save / cleanup
morphcloud devbox save <devbox-id> <name> [--json]
morphcloud devbox delete <devbox-id> [--json]# List available images
morphcloud image list [--json]# List all snapshots
morphcloud snapshot list [--json] [--metadata KEY=VALUE]
# Create a new snapshot
morphcloud snapshot create --image-id <id> --vcpus <n> --memory <mb> --disk-size <mb> [--digest <hash>] [--metadata KEY=VALUE]
# Get detailed snapshot information
morphcloud snapshot get <snapshot-id>
# Delete a snapshot
morphcloud snapshot delete <snapshot-id>
# Set metadata on a snapshot
morphcloud snapshot set-metadata <snapshot-id> KEY1=VALUE1 [KEY2=VALUE2...]# List all instances
morphcloud instance list [--json] [--metadata KEY=VALUE]
# Start a new instance from snapshot
morphcloud instance start <snapshot-id> [--json] [--metadata KEY=VALUE] [--ttl-seconds <seconds>] [--ttl-action stop|pause]
# Pause an instance
morphcloud instance pause <instance-id>
# Resume a paused instance
morphcloud instance resume <instance-id>
# Reboot an instance
morphcloud instance reboot <instance-id>
# Stop an instance
morphcloud instance stop <instance-id>
# Get instance details
morphcloud instance get <instance-id>
# Create snapshot from instance
morphcloud instance snapshot <instance-id> [--digest <hash>] [--json]
# Create multiple instances from an instance (branching)
morphcloud instance branch <instance-id> [--count <n>] [--json]
# Set metadata on an instance
morphcloud instance set-metadata <instance-id> KEY1=VALUE1 [KEY2=VALUE2...]# Execute command on instance
morphcloud instance exec <instance-id> <command>
# SSH into instance
morphcloud instance ssh <instance-id> [--rm] [--snapshot] [command]
# Port forwarding
morphcloud instance port-forward <instance-id> <remote-port> [local-port]
# Expose HTTP service
morphcloud instance expose-http <instance-id> <name> <port> [--auth-mode none|api_key]
# Hide HTTP service
morphcloud instance hide-http <instance-id> <name># Copy files to/from an instance
morphcloud instance copy <source> <destination> [--recursive]
# Examples:
# Local to remote
morphcloud instance copy ./local_file.txt inst_123:/remote/path/
# Remote to local
morphcloud instance copy inst_123:/remote/file.log ./local_dir/
# Copy directory recursively
morphcloud instance copy -r ./local_dir inst_123:/remote/dir# Start an interactive chat session with an instance
# Note: Requires ANTHROPIC_API_KEY environment variable
morphcloud instance chat <instance-id> [instructions]If you want to hack on the SDK/CLI locally:
# Clone the repository
git clone https://github.com/morph-labs/morph-python-sdk.git
cd morph-python-sdk
# Install in editable mode (SDK + CLI)
uv venv
source .venv/bin/activate
uv pip install -e . --extra dev --group devSet your API key as an environment variable:
# On macOS/Linux
export MORPH_API_KEY="your-api-key"
# On Windows (PowerShell)
$env:MORPH_API_KEY="your-api-key"
# On Windows (cmd)
set MORPH_API_KEY=your-api-keyfrom morphcloud.api import MorphCloudClient
# Initialize the client
client = MorphCloudClient()
# List available base images
print("\n\nAvailable base images:")
images = client.images.list()
for image in images:
print(f" {image.id}:\t{image.name}")
# Create a snapshot from a base image
print("\nCreating snapshot from base image...", end="")
snapshot = client.snapshots.create(
image_id="morphvm-minimal",
vcpus=1,
memory=512,
disk_size=1024
)
print("done")
# Start an instance from the snapshot
print("Starting instance from snapshot.....", end="")
instance = client.instances.start(snapshot_id=snapshot.id)
print("done")
# Wait for the instance to be ready
print("Waiting until instance is ready.....", end="")
instance.wait_until_ready()
print("done")
# Stop the instance when done
print("Stopping the instance...............", end="")
instance.stop()
print("done\n")from morphcloud.api import MorphCloudClient
client = MorphCloudClient()
snapshot = client.snapshots.create(vcpus=1, memory=512, disk_size=1024, image_id="morphvm-minimal")
# Using context managers for automatic cleanup
with client.instances.start(snapshot_id=snapshot.id) as instance:
instance.wait_until_ready()
# Connect via SSH and run commands
with instance.ssh() as ssh:
# Run a basic command
result = ssh.run("echo 'Hello from MorphCloud!'")
print(result.stdout)
# Install packages
ssh.run("apt-get update && apt-get install -y python3-pip").raise_on_error()
# Upload a local file to the instance
ssh.copy_to("./local_script.py", "/home/user/remote_script.py")
# Execute the uploaded script
ssh.run("python3 /home/user/remote_script.py")
# Download a file from the instance
ssh.copy_from("/home/user/results.txt", "./local_results.txt")import time
import requests
from morphcloud.api import MorphCloudClient
client = MorphCloudClient()
snapshot = client.snapshots.get("your_snapshot_id") # Use an existing snapshot
with client.instances.start(snapshot_id=snapshot.id) as instance:
instance.wait_until_ready()
with instance.ssh() as ssh:
# Start a simple HTTP server on the instance
ssh.run("python3 -m http.server 8080 &")
# Method 1: Expose as HTTP service with public URL
service_url = instance.expose_http_service("my-service", 8080)
print(f"Service available at: {service_url}")
# Method 2: Create an SSH tunnel for local port forwarding
with ssh.tunnel(local_port=8888, remote_port=8080):
time.sleep(1) # Give the tunnel time to establish
response = requests.get("http://localhost:8888")
print(response.text)Snapshot chains are the “build cache” part of Morph. Each exec() produces a new snapshot that includes the changes, so you can skip repeating expensive setup work.
from morphcloud.api import MorphCloudClient
client = MorphCloudClient()
base_snapshot = client.snapshots.get("your_base_snapshot_id")
# Each exec operation creates a new snapshot that includes the changes
# If you run the same command again, it will use the cached snapshot
python_snapshot = base_snapshot.exec("apt-get update && apt-get install -y python3 python3-pip")
numpy_snapshot = python_snapshot.exec("pip install numpy pandas matplotlib")
# Upload local files to a snapshot and create a new snapshot with those files
data_snapshot = numpy_snapshot.upload("./data/", "/home/user/data/", recursive=True)
# Run your analysis on the data
results_snapshot = data_snapshot.exec("python3 /home/user/data/analyze.py")
# Start an instance from the final snapshot with all changes applied
instance = client.instances.start(snapshot_id=results_snapshot.id)Set up instances that automatically redirect to Docker containers:
from morphcloud.api import MorphCloudClient
client = MorphCloudClient()
base_snapshot = client.snapshots.get("your_base_snapshot_id")
# Create a snapshot with a PostgreSQL container
postgres_snapshot = base_snapshot.as_container(
image="postgres:13",
container_name="postgres",
env={"POSTGRES_PASSWORD": "example"},
ports={5432: 5432}
)
# When you start an instance from this snapshot, all SSH sessions
# will automatically connect to the container instead of the host
with client.instances.start(snapshot_id=postgres_snapshot.id) as instance:
instance.wait_until_ready()
# This SSH session will connect directly to the container
with instance.ssh() as ssh:
ssh.run("psql -U postgres")Morph Cloud also provides asynchronous versions of all methods:
import asyncio
from morphcloud.api import MorphCloudClient
async def main():
client = MorphCloudClient()
# Async list images
images = await client.images.alist()
# Async create snapshot
snapshot = await client.snapshots.acreate(
image_id="morphvm-minimal",
vcpus=1,
memory=512,
disk_size=1024
)
# Async start instance
instance = await client.instances.astart(snapshot_id=snapshot.id)
# Async wait for ready
await instance.await_until_ready()
# Async stop instance
await instance.astop()
asyncio.run(main())Minimal examples for managing your account:
# List your API keys
morphcloud user api-key list
# Create a new API key (shows the key once)
morphcloud user api-key create
# Delete an API key
morphcloud user api-key delete <api_key_id>
# Get your SSH public key
morphcloud user ssh-key get
# Set/update your SSH public key
morphcloud user ssh-key set --public-key "ssh-rsa AAAA..."
# View usage (3-hour lookback by default; supports 30m, 3h, 7d, etc.)
morphcloud user usage --interval 3h
The CLI/SDK support named profiles for switching between prod/stage/dev environments without constantly exporting env vars.
# Create a profile
morphcloud profile set stage \
--api-key "$MORPH_API_KEY" \
--api-host "stage.morph.so"
# Use it for one command (kubectl-style)
morphcloud --profile stage instance list
# Or set it as the active profile
morphcloud profile use stageTo export env vars for other services/scripts:
eval "$(morphcloud profile env stage)"MORPH_API_KEY: Your Morph Cloud API keyMORPH_ENV: Convenience switch forprod/stagedefaults when no explicit host/base URL is providedMORPH_PROFILE: Select a named profileMORPH_BASE_URL: Override the default API URL (defaults to "https://cloud.morph.so/api")MORPH_API_HOST: Override API host used to derive defaults (e.g., "stage.morph.so")MORPH_SSH_HOSTNAME: Override the SSH hostname (defaults to "ssh.cloud.morph.so")MORPH_SSH_PORT: Override the SSH port (defaults to 22)MORPH_SERVICE_BASE_URL: Override the services API base URLMORPH_DEVBOX_BASE_URL: Override the devbox service base URL (defaults to "https://devbox.svc.<api_host>")MORPH_ADMIN_BASE_URL: Override the admin API base URLMORPH_DB_BASE_URL: Override the db API base URL
For issues, questions, or feature requests, please contact us at: [email protected]