> ## Documentation Index
> Fetch the complete documentation index at: https://b5bfb29-opencomputer.manicule.dev/llms.txt
> Use this file to discover all available pages before exploring further.
> For every page in one file, fetch https://b5bfb29-opencomputer.manicule.dev/llms-full.txt (large).
> To search and read these docs as tools, connect the MCP server at https://b5bfb29-opencomputer.manicule.dev/api/mcp.
> Every agent surface: https://b5bfb29-opencomputer.manicule.dev/.well-known/llms-manifest.json

# On Azure (/self-hosting/azure)

<!-- agent-signals: reading_time_min: 8 · est_tokens: 3600 · updated: 2026-11-13 -->
Related: [Start here](/self-hosting/overview.md), [On AWS](/self-hosting/aws.md), [On Google Cloud](/self-hosting/gcp.md)

> 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](/self-hosting/overview). 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 gateway                                                                                                              | You                                         |
| ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------- |
| Shows the Computer as `Online` while its VM is connected and `Offline` otherwise                                         | Create, 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 connects | Start the VM again                          |

The examples use two hostnames on your Cloudflare zone:

| Placeholder           | Serves                                                         |
| --------------------- | -------------------------------------------------------------- |
| `app.example.com`     | The app Worker: dashboard, sign-in, Slack and GitHub callbacks |
| `gateway.example.com` | The gateway Worker: sessions and the VMs' WebSocket            |

## 1. Get the code

```bash
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

```bash
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:

| Setting                   | Value                                                                     |
| ------------------------- | ------------------------------------------------------------------------- |
| OAuth redirect URL        | `https://app.example.com/api/managed-agents/computer/slack/callback`      |
| Event request URL         | `https://app.example.com/api/managed-agents/computer/slack/events`        |
| Interactivity request URL | `https://app.example.com/api/managed-agents/computer/slack/interactivity` |

Bot token scopes:

```text
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:

| Setting                | Value                                                                                   |
| ---------------------- | --------------------------------------------------------------------------------------- |
| Callback URL           | `https://app.example.com/api/managed-agents/github/callback`                            |
| Webhook URL            | `https://app.example.com/api/managed-agents/github/webhooks`                            |
| Repository permissions | Metadata: read. Contents: read and write. Pull requests: read and write. Checks: write. |
| Events                 | Installation, 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:

| Field                                                                      | Value                                                                                                      |
| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `name`, `account_id`, `routes`                                             | Your Worker name, account, and `gateway.example.com` as a custom domain                                    |
| `COMPUTER_VMS`                                                             | Add it, with the value `self-managed`                                                                      |
| `COMPUTER_PUBLIC_ORIGIN`, `OPENCOMPUTER_APP_URL`                           | `https://app.example.com`                                                                                  |
| `SLACK_COMPUTER_CLIENT_ID`                                                 | Add it, with the Slack client ID                                                                           |
| `OC_GITHUB_APP_ID`, `OC_GITHUB_APP_SLUG`, `OC_GITHUB_APP_CLIENT_ID`        | From the GitHub App                                                                                        |
| D1, KV and R2 bindings                                                     | The `computer-gateway` database, `WIRE_KEYS` namespace and `managed-agents-session-log` bucket from step 2 |
| `services`, `tail_consumers`                                               | Remove both blocks                                                                                         |
| `COMPUTER_HOST_GATEWAY_URL`, `COMPUTER_DEFAULT_SIZE`, `COMPUTER_POOL_SIZE` | Remove. They are not used.                                                                                 |

Write the secrets to a JSON file with mode `0600`:

| Secret                                                                                                                 | Value                          |
| ---------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
| `OC_MANAGED_AGENTS_SECRET`, `OC_MANAGED_CRED_HMAC_SECRET`, `BLUE_USAGE_HMAC_SECRET`, `BLUE_SESSION_INDEX_ADMIN_SECRET` | `openssl rand -hex 32` each    |
| `BLUE_CHANNEL_ENCRYPTION_KEY`, `COMPUTER_HOST_TOKEN_KEY`                                                               | `openssl rand -base64 32` each |
| `SLACK_COMPUTER_CLIENT_SECRET`, `SLACK_COMPUTER_SIGNING_SECRET`                                                        | From the Slack app             |
| `OC_GITHUB_APP_PRIVATE_KEY`, `OC_GITHUB_APP_CLIENT_SECRET`, `OC_GITHUB_APP_WEBHOOK_SECRET`                             | From the GitHub App            |
| `COMPUTER_ANTHROPIC_API_KEY`                                                                                           | Your Anthropic API key         |

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

```bash
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:

| Secret                                                                           | Value                                  |
| -------------------------------------------------------------------------------- | -------------------------------------- |
| `WORKOS_API_KEY`, `WORKOS_CLIENT_ID`                                             | From WorkOS                            |
| `OC_MANAGED_AGENTS_SECRET`, `OC_MANAGED_CRED_HMAC_SECRET`                        | The same values as the gateway         |
| `AGENT_RUNTIME_USAGE_HMAC_SECRET`                                                | The gateway's `BLUE_USAGE_HMAC_SECRET` |
| `SESSION_JWT_SECRET`, `CF_ADMIN_SECRET`, `SECRET_ENCRYPTION_KEY`, `EVENT_SECRET` | `openssl rand -hex 32` each            |

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

```bash
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:

```bash
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`:

```bash
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:

```bash
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.

```bash
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:

```bash
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:

```bash
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"
```
