Skip to content

On Azure

Deploy the app and gateway Workers on Cloudflare, and create each organization's Computer VM in Azure yourself

This page deploys the stack described in Start here. The gateway runs with COMPUTER_VMS set to self-managed: it creates no VMs. You create one Azure VM per organization, and the VM connects out to the gateway with that organization’s host token.

The gatewayYou
Shows the Computer as Online while its VM is connected and Offline otherwiseCreate, size, stop, start and delete the VM
Holds a Slack message that arrives while the VM is offline, says so once in the thread, and runs it when the VM connectsStart the VM again

The examples use two hostnames on your Cloudflare zone:

PlaceholderServes
app.example.comThe app Worker: dashboard, sign-in, Slack and GitHub callbacks
gateway.example.comThe gateway Worker: sessions and the VMs’ WebSocket

1. Get the code

git clone https://github.com/diggerhq/opencomputer.git
cd opencomputer
(cd services/managed-agents && npm ci)
(cd cloudflare-workers/api-edge && npm ci)
(cd web && npm ci)

2. Create the Cloudflare resources

npx wrangler login
npx wrangler d1 create computer-app
npx wrangler d1 create computer-gateway
npx wrangler kv namespace create WIRE_KEYS
npx wrangler kv namespace create SESSIONS_KV
npx wrangler r2 bucket create managed-agents-session-log

Note each ID; the next steps put them into the Workers’ configuration files.

3. Create the WorkOS, Slack and GitHub apps

The URLs below use https://app.example.com.

WorkOS. Create an application with redirect URI https://app.example.com/auth/callback, and set the sign-out redirect to https://app.example.com in the WorkOS dashboard. Keep the API key and client ID.

Slack. Create a Slack app with:

SettingValue
OAuth redirect URLhttps://app.example.com/api/managed-agents/computer/slack/callback
Event request URLhttps://app.example.com/api/managed-agents/computer/slack/events
Interactivity request URLhttps://app.example.com/api/managed-agents/computer/slack/interactivity

Bot token scopes:

app_mentions:read assistant:write channels:history channels:read channels:manage
groups:history groups:read groups:write im:history im:read im:write chat:write
files:read files:write reactions:write users:read users:read.email

Bot events: app_mention, message.im, message.channels, message.groups, app_uninstalled, tokens_revoked, team_leave, user_change, and agent_session_stopped if Slack offers it. Keep the client ID, client secret and signing secret.

GitHub. Create a GitHub App with:

SettingValue
Callback URLhttps://app.example.com/api/managed-agents/github/callback
Webhook URLhttps://app.example.com/api/managed-agents/github/webhooks
Repository permissionsMetadata: read. Contents: read and write. Pull requests: read and write. Checks: write.
EventsInstallation, Installation repositories, Repository, Push, Pull request

Generate a private key. Keep the app ID, slug, client ID, client secret, webhook secret and private key.

4. Deploy the gateway Worker

Start from services/managed-agents/wrangler.production.jsonc and edit:

FieldValue
name, account_id, routesYour Worker name, account, and gateway.example.com as a custom domain
COMPUTER_VMSAdd it, with the value self-managed
COMPUTER_PUBLIC_ORIGIN, OPENCOMPUTER_APP_URLhttps://app.example.com
SLACK_COMPUTER_CLIENT_IDAdd it, with the Slack client ID
OC_GITHUB_APP_ID, OC_GITHUB_APP_SLUG, OC_GITHUB_APP_CLIENT_IDFrom the GitHub App
D1, KV and R2 bindingsThe computer-gateway database, WIRE_KEYS namespace and managed-agents-session-log bucket from step 2
services, tail_consumersRemove both blocks
COMPUTER_HOST_GATEWAY_URL, COMPUTER_DEFAULT_SIZE, COMPUTER_POOL_SIZERemove. They are not used.

Write the secrets to a JSON file with mode 0600:

SecretValue
OC_MANAGED_AGENTS_SECRET, OC_MANAGED_CRED_HMAC_SECRET, BLUE_USAGE_HMAC_SECRET, BLUE_SESSION_INDEX_ADMIN_SECRETopenssl rand -hex 32 each
BLUE_CHANNEL_ENCRYPTION_KEY, COMPUTER_HOST_TOKEN_KEYopenssl rand -base64 32 each
SLACK_COMPUTER_CLIENT_SECRET, SLACK_COMPUTER_SIGNING_SECRETFrom the Slack app
OC_GITHUB_APP_PRIVATE_KEY, OC_GITHUB_APP_CLIENT_SECRET, OC_GITHUB_APP_WEBHOOK_SECRETFrom the GitHub App
COMPUTER_ANTHROPIC_API_KEYYour Anthropic API key

Keep the value of COMPUTER_HOST_TOKEN_KEY at hand: step 7 derives each organization’s host token from it.

cd services/managed-agents
npx wrangler d1 migrations apply computer-gateway --remote --config wrangler.production.jsonc
npx wrangler deploy --config wrangler.production.jsonc --secrets-file gateway-secrets.json
curl https://gateway.example.com/health

5. Deploy the app Worker

In cloudflare-workers/api-edge/wrangler.toml:

  • Set name, account_id, and the route to app.example.com.
  • Set the D1 binding to computer-app and the KV binding to SESSIONS_KV.
  • Set the CREDIT_ACCOUNT binding’s script_name to this Worker’s name.
  • Set COMPUTER_ENABLED_FOR_ALL = "true" and MANAGED_AGENTS_API_URL = "https://gateway.example.com".
  • Remove tail_consumers.

Secrets, in a second 0600 JSON file:

SecretValue
WORKOS_API_KEY, WORKOS_CLIENT_IDFrom WorkOS
OC_MANAGED_AGENTS_SECRET, OC_MANAGED_CRED_HMAC_SECRETThe same values as the gateway
AGENT_RUNTIME_USAGE_HMAC_SECRETThe gateway’s BLUE_USAGE_HMAC_SECRET
SESSION_JWT_SECRET, CF_ADMIN_SECRET, SECRET_ENCRYPTION_KEY, EVENT_SECRETopenssl rand -hex 32 each

Build the dashboard into the Worker’s assets, create the schema and deploy:

cd web && npm run build && cp -r dist/. ../cloudflare-workers/api-edge/assets/
cd ../cloudflare-workers/api-edge
npx wrangler d1 execute computer-app --remote -c wrangler.toml --file schema-snapshots/current_schema.sql
npx wrangler d1 migrations apply computer-app --remote -c wrangler.toml
npx wrangler deploy -c wrangler.toml --secrets-file app-secrets.json
curl https://app.example.com/health

6. Build and upload the Computer release

From the repository root:

computer/deploy/bundle.sh /tmp/calvin.bundle

az login
az account set --subscription '<SUBSCRIPTION>'

export REGION=eastus
export RG=computers
export STORAGE_ACCOUNT=<globally unique, lowercase>

az group create -n "$RG" -l "$REGION"
az storage account create -n "$STORAGE_ACCOUNT" -g "$RG" -l "$REGION" \
  --sku Standard_LRS --allow-blob-public-access false
az storage container create --account-name "$STORAGE_ACCOUNT" -n bootstrap --auth-mode key
az storage blob upload --account-name "$STORAGE_ACCOUNT" -c bootstrap \
  -n calvin.bundle -f /tmp/calvin.bundle --auth-mode key --overwrite

The bundle holds the Computer source and the model proxy built for Linux amd64. The container stays private; step 8 gives the VM a URL that is valid for one hour.

7. Get the organization ID and make the host token

  1. Open https://app.example.com and sign in.
  2. Open Settings and copy the Organization ID.
  3. Derive the organization’s host token from COMPUTER_HOST_TOKEN_KEY:
export ORG_ID='<ORGANIZATION_ID>'
export COMPUTER_HOST_TOKEN_KEY='<the gateway secret>'

export TOKEN="sph_$(printf 'calvin-host:%s' "$ORG_ID" \
  | openssl dgst -sha256 -hmac "$COMPUTER_HOST_TOKEN_KEY" -binary \
  | base64 | tr '+/' '-_' | tr -d '=')"

The token is valid for that organization only, and for as long as the key does not change.

8. Create the VM

The VM is Ubuntu 24.04 on amd64 with a second, blank disk for /data. It needs outbound internet access and no inbound access.

Make a download URL for the release, then write the VM’s first-boot script with self-managed-user-data.sh. The script it prints stores the gateway URL and the token in a root-only file on the VM, downloads the release and runs its boot script:

EXPIRY="$(date -u -d '+1 hour' +%Y-%m-%dT%H:%MZ 2>/dev/null || date -u -v+1H +%Y-%m-%dT%H:%MZ)"
export BUNDLE_URL="$(az storage blob generate-sas --account-name "$STORAGE_ACCOUNT" \
  -c bootstrap -n calvin.bundle --permissions r --expiry "$EXPIRY" \
  --https-only --full-uri --auth-mode key -o tsv)"

(umask 077; GATEWAY_HOST=gateway.example.com \
  computer/deploy/vm/self-managed-user-data.sh > computer-user-data.sh)

Create the VM. az vm create also creates a virtual network, a public IP address for outbound traffic, and a network security group; --nsg-rule NONE leaves that group without a rule that allows inbound traffic.

az vm create -g "$RG" -n computer-1 -l "$REGION" \
  --image Canonical:ubuntu-24_04-lts:server:latest \
  --size Standard_D2as_v5 \
  --os-disk-size-gb 64 --data-disk-sizes-gb 128 \
  --nsg-rule NONE \
  --admin-username azureuser --generate-ssh-keys \
  --custom-data computer-user-data.sh \
  --tags organization="$ORG_ID"

Any amd64 size with 2 vCPUs and 8 GiB of memory or more works; az vm list-skus -l "$REGION" --size Standard_D2 -o table lists the sizes your subscription can use.

The first boot installs the toolchains, formats the blank disk, mounts it at /data and starts the host. It takes about six minutes. The boot script formats a disk only when it is blank.

The token stays in the VM’s custom data. On the VM only root can read it; Azure does not return custom data once the VM exists.

9. Add the Slack app and connect GitHub

  1. Open https://app.example.com. The Computer shows Online once the VM has connected.
  2. Click Add to Slack and approve.
  3. Connect GitHub and choose the repositories.
  4. Mention @computer in a channel.

Repeat steps 7 and 8 for each further organization, with another VM name.

Stop, start and resize

Stop the VM with az vm deallocate; the Computer shows Offline and Slack messages wait. Start it with az vm start; the host reconnects and the workspace is still on /data. To resize, run az vm resize with the new size.

az vm delete does not delete the data disk.

Check a VM

No rule allows SSH. The boot log is /var/log/calvin-boot.log, and the two services are slack-presence-host and oc-modelproxy. Run checks with Azure Run Command:

az vm run-command invoke -g "$RG" -n computer-1 --command-id RunShellScript --scripts \
  "systemctl is-active slack-presence-host.service oc-modelproxy.service; tail -50 /var/log/calvin-boot.log"

Update a VM

A self-managed VM does not update itself. Build and upload a new bundle (step 6), make a new BUNDLE_URL (step 8), and run:

az vm run-command invoke -g "$RG" -n computer-1 --command-id RunShellScript --scripts "
set -e
curl -fsSL --retry 5 -o /opt/bootstrap/calvin.bundle '$BUNDLE_URL'
git -C /opt/bootstrap/calvin-src fetch -q /opt/bootstrap/calvin.bundle main
git -C /opt/bootstrap/calvin-src checkout -q -f --detach FETCH_HEAD
CODE_ONLY=1 BOOTSTRAP_FETCH=0 bash /opt/bootstrap/calvin-src/deploy/vm/azure-computer-boot.sh"