How to deploy a capsule to the cloud
Beta feature
mur deploy is a beta feature and is hidden by default. Enable it first with
mur beta enable deploy. The same flag also enables mur deploy ls and the
teardown counterpart mur destroy (both used below) — they are gated together.
Behavior and flags may change in future releases.
mur deploy run uploads your capsule files to an existing VM via SSH, starts the capsule, and returns a public A2A endpoint. You create and manage the VM through your cloud provider's dashboard — mur deploy run never provisions or terminates VMs on your behalf.
The relevant manifest options are:
| Option | Controls |
|---|---|
| deploy.mur_version | Pins the exact mur binary version installed on the VM |
| inference.driver.artifact | Inference driver — required for agent capsules |
| inference.system_prompt_file | Local file uploaded alongside the manifest as the system prompt |
| lifecycle.task_acceptance | Whether the deployed capsule accepts incoming A2A tasks |
| lifecycle.after_task | What the capsule does after a task completes |
Prerequisites
- A working agent capsule that runs locally with
mur run(aninferenceblock is required) - A VM you have created through your cloud provider, with SSH access and a public IP address
Step 1 — create a VM
Log in to your cloud provider's control panel and create a new Linux VM. Supported providers include Hostinger, Hetzner, DigitalOcean, AWS, and any other provider that gives you SSH access and a public IP.
Choose Ubuntu 22.04 LTS or 24.04 LTS as the operating system. All major providers offer it. Other distributions (Debian, Rocky Linux, AlmaLinux, Fedora Cloud) also work. Alpine Linux is only compatible if you build mur with the x86_64-unknown-linux-musl target.
Make a note of the VM's public IP address before continuing.
Step 2 — pin the mur version (recommended)
Add a deploy: block with a mur_version field to your murmur.yaml:
name: my-agent
version: "0.1.0"
deploy:
mur_version: "X.Y.Z"
artifacts:
- name: murmur/driver-anthropic
version: "0.19.0"
runtime: driver
gateway:
endpoint: https://api.anthropic.com
api_key: ${ANTHROPIC_API_KEY}
inference:
transport: http
model: claude-sonnet-5
driver:
artifact: murmur/driver-anthropic
When deploy.mur_version is set, mur deploy run downloads that exact binary from GitHub releases and caches it at ~/.murmur/bin/mur-{version}-linux-x86_64. Subsequent deploys with the same version skip the download entirely.
If deploy.mur_version is omitted, the version of the mur binary running on your machine is used instead. mur run reads nothing in deploy:, so the pin has no effect on local runs.
Pin a release that has mur precompile. With an older one the deploy prints W-DEPLOY-001 and the capsule compiles its artifacts on its first launch.
Step 3 — deploy
mur deploy run \
--host 1.2.3.4 \
--manifest ./murmur.yaml \
--env ANTHROPIC_API_KEY=sk-ant-...
mur deploy run shows every step upfront as pending · before any work starts.
Steps use a two-level hierarchy: the group header at level 1, individual items indented at level 2.
When a group finishes, the level-2 items collapse — only the checkmarked summary stays.
Initial state (everything pending):
· ↓ 1 artifact 1 to fetch
· ↓ murmur/driver-anthropic@0.19.0
· ↓ mur vX.Y.Z
· SSH
· ↑ mur binary
· ↑ files
· ↑ 1 artifact
· ↑ murmur/driver-anthropic@0.19.0
· ⚙ compile 1 artifact
· → start capsule
During downloads, each artifact being fetched shows a bytes / total bar:
⠸ ↓ 1 artifact
⠸ ↓ murmur/driver-anthropic@0.19.0 1.38 MiB/2.90 MiB [██████░░░░░░]
· ↓ mur vX.Y.Z
...
After each group completes, level-2 items collapse. SSH disappears once the VM answers, before the first upload. ⚙ compile counts what the VM's mur precompile did with each artifact:
✓ ↓ 1 artifact 3 MB
✓ ↓ mur vX.Y.Z cached
✓ ↑ mur binary 59 MB
✓ ↑ murmur.yaml
✓ ↑ 1 artifact 3 MB
✓ ⚙ compile 1 artifact 1 compiled · 0 stored · 0 not wasm · 0 failed 0.5s
✓ → capsule started · port 9000
┌──────────────────────────────────────────┐
│ ∞ my-agent │
│ │
│ url http://1.2.3.4:9000 │
│ dep dep_01954a3b │
│ time 47s │
└──────────────────────────────────────────┘
All output goes to stderr. Save the deployment ID from the box — you will need it for mur deploy ls and mur destroy.
All flags
| Flag | Default | Description |
|---|---|---|
--host |
(required) | VM public IP address or hostname |
--manifest |
./murmur.yaml |
Path to the capsule manifest |
--ssh-user |
root |
SSH username on the VM |
--ssh-key |
(SSH agent) | Path to SSH private key; falls back to SSH agent / ~/.ssh/id_* |
--mur-binary |
(auto-download) | Path to a pre-built Linux x86_64 mur binary; skips the GitHub download |
--env |
(none) | KEY=VALUE environment variable for mur run on the VM; repeat for multiple |
--env-file |
(auto-detect) | Path to a .env file; KEY=VALUE or export KEY=VALUE; # comments ignored. If neither --env nor --env-file is passed, .env next to the manifest is loaded automatically |
--workdir |
(none) | Local directory to upload as the capsule working directory |
--deploy-platform |
linux-x86_64 |
Target platform for artifact resolution and staging |
--no-precompile |
(off) | Skip compiling the uploaded WASM artifacts on the VM; the capsule compiles them on its first launch instead |
What mur deploy run does
- Parses the manifest and resolves every declared artifact for the target platform — fails immediately if any artifact is unavailable, before touching the network. A native artifact installed for this host is not reused for a different target: the target platform's payload is downloaded and cached beside it
- Checks that every file referenced by the manifest (e.g.
inference.system_prompt_file) exists locally — fails immediately if any file is missing - Validates every
--enventry; a missing=or empty key causes an immediate exit - Resolves the
murbinary: usesdeploy.mur_versionfrom the manifest if set, otherwise the running version; downloads and caches if not already cached - Waits up to 30 seconds for SSH to become available on the VM
- Uploads the
murbinary to/usr/local/bin/mur - Uploads the manifest, any manifest-referenced local files, and the optional workdir
- Uploads all pre-staged artifacts to
/root/.murmur/artifacts/ - Writes env vars to
/root/mur-{short_id}/.envwith mode600; env vars come from--env,--env-file, or.envnext to the manifest (auto-detected); skipped when none are set - Sources
/root/mur-{short_id}/.env(if present) and runsmur precompileover the uploaded artifacts with the VM's ownmur, so the capsule's first launch loads them compiled; about half a second for a small capsule, taken off the start that follows. Skipped with--no-precompile. A VM that cannot precompile printsW-DEPLOY-001and the deploy continues - Sources
/root/mur-{short_id}/.env(if present) and startsmur run --manifest <path> --json; waits up to 120 seconds for startup - Appends the deployment record to
~/.murmur/deployments.json
Steps 1–4 all run locally before any SSH connection. A bad manifest, missing artifact, missing file, or malformed --env entry causes mur deploy run to exit with a clear error message and no VM interaction.
Step 4 — verify the agent card
Once mur deploy run returns, the capsule's A2A endpoint is publicly reachable. Confirm it is alive by fetching the agent card:
curl http://1.2.3.4:9000/.well-known/agent-card.json
A 200 response confirms the capsule is running:
{
"name": "my-agent",
"description": "Murmur capsule my-agent 0.1.0",
"version": "0.1.0",
"supportedInterfaces": [
{ "url": "http://localhost:9000", "protocolBinding": "JSONRPC", "protocolVersion": "1.0" }
],
"capabilities": {
"streaming": true,
"pushNotifications": false,
"extendedAgentCard": false,
"extensions": [
{
"uri": "https://docs.murmur.nexus/reference/agent-card/#murmur-door-v1",
"description": "Every JSON-RPC method this door answers, A2A methods and murmur's own alike, the murmur ones named murmur.<Method>; whether it accepts tasks from peer capsules; and the forgetSession field a message's metadata may carry under this URI.",
"required": false,
"params": {
"methods": ["SendMessage", "SendStreamingMessage", "murmur.WatchSession", "GetTask", "ListTasks", "CancelTask", "SubscribeToTask", "murmur.StopSession"],
"peerTasks": false
}
},
{
"uri": "https://docs.murmur.nexus/reference/agent-card/#murmur-capsule-v1",
"description": "The session answering this address and what the capsule may do. Served only to authenticated callers once the door authenticates.",
"required": false,
"params": {
"sessionId": "ses_019f01a940ce7761854e768ecbe3d399",
"tools": [],
"shell": false,
"network": false,
"planes": []
}
},
{
"uri": "https://docs.murmur.nexus/reference/streaming-protocol/#murmur-stream-v1",
"description": "The murmur frames this capsule writes in addition to the A2A v1.0 events: sent on SendStreamingMessage and SubscribeToTask when the request's A2A-Extensions header names this URI, and always on murmur.WatchSession. Each is carried in a StreamResponse under metadata keyed by this URI.",
"required": false,
"params": {
"frames": ["text", "thinking", "tool-call-started", "tool-call-progress", "artifact", "gap", "lagged", "connection-ack", "capsule-closed"]
}
}
]
},
"securitySchemes": {},
"securityRequirements": [],
"defaultInputModes": ["text/plain", "application/json"],
"defaultOutputModes": ["text/plain", "application/json"],
"skills": [
{
"id": "task",
"name": "Run a task",
"description": "Runs one task given as a text message and reports its outcome.",
"tags": ["task"]
}
]
}
The card names the address the capsule's listener answers on inside the VM, localhost:9000;
reach it from outside at the VM's public IP. See Agent Card for what
each key means.
Step 5 — send a task
The deployed capsule accepts standard A2A messages at its public URL. Send a task the same way you would to a local capsule:
curl -s -X POST http://1.2.3.4:9000 \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "SendMessage",
"params": {
"message": {
"messageId": "msg-001",
"role": "ROLE_USER",
"parts": [{"text": "Hello from the cloud."}]
}
}
}'
The door answers once the task has finished, with the task and its answer in its response
artifact. Add "configuration": {"returnImmediately": true} to params to be answered as soon as
the task is accepted instead, then poll GetTask — see
Agent Card: Answers. See
Connect two capsules with A2A messaging for the full message protocol.
Under the default lifecycle, task_acceptance: single with after_task: exit, the capsule runs one incoming task and then exits. To keep a deployed capsule answering task after task, declare task_acceptance: queue with after_task: sleep. Under task_acceptance: none the door does not serve SendMessage.
Step 6 — list deployments
mur deploy ls
DEPLOYMENT_ID PROVIDER REGION STATUS URL
----------------------------------------------------------------------------------------------------
dep_01954a3b5c7d8e9f0a1b2c3d4e5f6a7b manual running http://1.2.3.4:9000
All active deployments are stored in ~/.murmur/deployments.json. Each row maps to one entry.
Step 7 — remove from the deployment list
When you no longer need a deployment, remove it from the tracking list:
mur destroy dep_01954a3b5c7d8e9f0a1b2c3d4e5f6a7b
This removes the entry from ~/.murmur/deployments.json. It does not stop or delete the VM — shut down the VM from your cloud provider's dashboard separately.
Reference: using a system prompt file
If your capsule reads its system prompt from a file rather than inline YAML, set inference.system_prompt_file in your manifest:
inference:
transport: http
model: claude-sonnet-5
system_prompt_file: instructions.md
driver:
artifact: murmur/driver-anthropic
mur deploy run automatically detects instructions.md as a referenced local file, verifies it exists before attempting any SSH connection, and uploads it alongside the manifest to the remote deploy directory. No additional flags required.
Reference: passing environment variables
Use --env KEY=VALUE to inject runtime secrets — API keys, tokens, database URLs — into the capsule's environment on the VM. Pass one flag per variable:
mur deploy run \
--host 1.2.3.4 \
--manifest ./murmur.yaml \
--env ANTHROPIC_API_KEY=sk-ant-... \
--env DATABASE_URL=postgres://user:pass@host/db
Or point to a .env file with --env-file:
mur deploy run --host 1.2.3.4 --env-file ./secrets.env
If neither --env nor --env-file is provided, mur deploy run looks for a .env file in the same directory as the manifest and loads it automatically when found. This means running mur deploy run --host 1.2.3.4 from a project folder that has a .env file works with no extra flags.
Both KEY=VALUE and export KEY=VALUE formats are accepted; the export prefix is stripped automatically.
In the manifest, reference variables with ${VAR_NAME}:
artifacts:
- name: murmur/driver-anthropic
version: "0.19.0"
runtime: driver
gateway:
endpoint: https://api.anthropic.com
api_key: ${ANTHROPIC_API_KEY}
When the capsule starts, mur run reads ANTHROPIC_API_KEY from its environment and substitutes it wherever ${ANTHROPIC_API_KEY} appears in the manifest. If the variable is not set, mur run exits with E-MAN-003.
mur deploy run writes all --env values to /root/mur-{short_id}/.env on the VM with mode 600 and sources that file before starting mur run. Values are transferred using base64 encoding, so values that contain spaces, equals signs, newlines, or shell metacharacters are preserved exactly. When no --env flags are passed, no .env file is written.
Every --env entry is validated before any SSH connection is made. An entry with no = or an empty key causes mur deploy run to exit immediately with an error.
Reference: using a pre-built binary
If you have already built a Linux mur binary — for example, on a CI server — pass it directly instead of letting mur deploy run download one:
mur deploy run \
--host 1.2.3.4 \
--manifest ./murmur.yaml \
--mur-binary ./target/x86_64-unknown-linux-musl/release/mur
To build the musl binary locally:
rustup target add x86_64-unknown-linux-musl
CARGO_PROFILE_DEV_DEBUG=0 cargo build --release \
--target x86_64-unknown-linux-musl \
-p murmur-cli
musl vs glibc
x86_64-unknown-linux-musl produces a fully static binary that runs on every Linux distribution including Alpine. x86_64-unknown-linux-gnu (glibc) works on Ubuntu, Debian, Rocky, and AlmaLinux, but fails on Alpine.
Summary
| Feature | Detail |
|---|---|
| Command | mur deploy run --host <IP> --manifest <path> |
| Progress | All steps shown upfront as pending; each activates in turn; artifact downloads show bytes/total bar; artifact uploads run in parallel (stderr) |
| Output | Success summary box on stderr showing url, deployment id (dep_ prefix + 8 hex chars), and elapsed time |
| VM management | You create and delete VMs through your cloud provider; mur deploy run never provisions or terminates VMs |
| Recommended OS | Ubuntu 22.04 LTS or 24.04 LTS |
| mur binary | Auto-downloaded from GitHub releases; pin with deploy.mur_version in the manifest |
| Manifest files | inference.system_prompt_file and any other referenced local files are uploaded automatically |
| Env vars | --env KEY=VALUE (repeatable) · --env-file <path> · auto-loads .env next to the manifest if neither flag is passed |
| Artifact pre-staging | All manifest artifacts are resolved and uploaded before the capsule starts |
| Timeout | 30s SSH wait + 120s capsule startup wait |
| State file | ~/.murmur/deployments.json — appended on deploy, entry removed on mur destroy |
| Health check | GET {url}/.well-known/agent-card.json returns 200 when the capsule is alive |
| Tear down | mur destroy <deployment_id> removes the tracking entry; delete the VM from your provider dashboard |
| List | mur deploy ls — reads ~/.murmur/deployments.json |