Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions deploy/docker-compose/.env.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,18 @@ HOST_CLOUDSDK_CONFIG=

OPS_CODE_BLOCK_MEMORY_LIMIT_IN_MB=256
OPS_SLACK_ENABLE_INTERACTIONS=true

# ---------------------------------------------------------
# MCP server for external agents (opt-in)
# ---------------------------------------------------------

# Set OPS_OAUTH_ENABLED=true and uncomment COMPOSE_PROFILES=mcp to serve the public URL
# plus /mcp to agents such as Claude Code with a plain "docker compose up -d".
# OPS_PUBLIC_URL must be https unless it is localhost. The client secret has no default:
# set it to a random value of at least 32 characters, e.g. the output of "openssl rand -hex 32".
# The MCP container refuses to start while it is empty.
OPS_OAUTH_ENABLED=false
OPS_OAUTH_ISSUER_URL=${OPS_PUBLIC_URL}/api
OPS_MCP_RESOURCE_URL=${OPS_PUBLIC_URL}/mcp
OPS_OAUTH_RS_CLIENT_SECRET=
# COMPOSE_PROFILES=mcp
60 changes: 50 additions & 10 deletions deploy/docker-compose/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,51 @@

# Docker Compose Deployment

This is a docker compose deployment of the OpenOps platform.


# Installation

See the [getting started guide](https://docs.openops.com/getting-started/deployment/local) for local deployment in our documentation.

# MCP server for external agents

External agents such as Claude Code or Codex connect to OpenOps through the `openops-mcp`
container, which is disabled by default. It sits behind the gateway at `${OPS_PUBLIC_URL}/mcp`
and authenticates agents with OAuth issued by the OpenOps API.

To enable it, in `.env`:

- `OPS_PUBLIC_URL` must be `https://...` (plain `http` is only accepted for `localhost`), so
enable TLS first.
- `OPS_OAUTH_ENABLED=true` turns on OAuth in the app. The issuer and resource URLs derive from
`OPS_PUBLIC_URL` and normally need no change.
- `OPS_OAUTH_RS_CLIENT_SECRET` is empty by default and must be set to a random value of at least
32 characters, for example `openssl rand -hex 32`. The MCP container refuses to start without it.
- `COMPOSE_PROFILES=mcp` enables the `mcp` compose profile, so the usual `docker compose up -d`
(and the install script) also pulls and starts the MCP container.
- The MCP image version is pinned in `docker-compose.yml` and pulled from the public
`openops.azurecr.io` registry.

Then restart the stack:

```bash
docker compose up -d
```

If you prefer not to set `COMPOSE_PROFILES` in `.env`, pass the profile on the command line
instead: `docker compose --profile mcp up -d`.

Existing installations keep their `.env` across upgrades (the install script never rewrites
it), so the `OPS_OAUTH_*`, `OPS_MCP_RESOURCE_URL` and `COMPOSE_PROFILES` lines from
`.env.defaults` have to be copied into `.env` by hand before enabling MCP. The install script
does not generate `OPS_OAUTH_RS_CLIENT_SECRET` either; always set it yourself.

Connect an agent to `${OPS_PUBLIC_URL}/mcp`, for example:

```bash
claude mcp add --transport http openops https://<host>/mcp
```

The first tool call opens a browser to approve the connection under Settings → Connected apps.

# Connections

Expand All @@ -17,15 +55,15 @@ OpenOps supports Redis over TLS for deployments where Redis is not running on th
same private Docker network. The application and worker accept the following
environment variables (prefixed with `OPS_` in the deployment environment):

| Variable | Description |
| --- | --- |
| `OPS_REDIS_URL` | Complete ioredis connection URL. When set, it takes precedence over the host/port settings. |
| `OPS_REDIS_HOST` | Redis hostname, used when `OPS_REDIS_URL` is not set. |
| `OPS_REDIS_PORT` | Redis port, used with `OPS_REDIS_HOST`. |
| `OPS_REDIS_USE_SSL` | Set to `true` to enable TLS when using host/port settings. |
| `OPS_REDIS_USER` | Optional Redis username. |
| Variable | Description |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `OPS_REDIS_URL` | Complete ioredis connection URL. When set, it takes precedence over the host/port settings. |
| `OPS_REDIS_HOST` | Redis hostname, used when `OPS_REDIS_URL` is not set. |
| `OPS_REDIS_PORT` | Redis port, used with `OPS_REDIS_HOST`. |
| `OPS_REDIS_USE_SSL` | Set to `true` to enable TLS when using host/port settings. |
| `OPS_REDIS_USER` | Optional Redis username. |
| `OPS_REDIS_PASSWORD` | Redis password or auth token. Store it in the deployment secret store rather than committing it to an environment file. |
| `OPS_REDIS_DB` | Redis database number; defaults to `0`. |
| `OPS_REDIS_DB` | Redis database number; defaults to `0`. |

For an ElastiCache deployment with transit encryption required, use
`OPS_REDIS_USE_SSL=true` together with `OPS_REDIS_HOST`, `OPS_REDIS_PORT`, and
Expand All @@ -41,6 +79,7 @@ To use the Azure CLI block, you need to create a connection to Azure. If you use

However, it is possible to share your local session with the platform for local applications.
To do this, you need to set two environment variables:

- `OPS_ENABLE_HOST_SESSION=true`: enables sharing of the host session with the platform container.
- `HOST_AZURE_CONFIG_DIR=/root/.azure`: defines the path to the host machine's Azure configuration folder that will be shared with the platform container

Expand All @@ -50,5 +89,6 @@ To use the Google Cloud CLI block, you need to create a connection to Google Clo

However, it is possible to share your local session with the platform for local applications.
To do this, you need to set two environment variables:

- `OPS_ENABLE_HOST_SESSION=true`: enables sharing of the host session with the platform container.
- `HOST_CLOUDSDK_CONFIG=/root/.config/gcloud`: defines the path to the host machine's Google Cloud configuration folder that will be shared with the platform container
44 changes: 43 additions & 1 deletion deploy/docker-compose/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ services:
OPS_OPENOPS_TABLES_VERSION: 0.2.22
OPS_ANALYTICS_VERSION: 0.14.8
OPS_WORKER_URL: http://openops-worker:3000
healthcheck:
test: ['CMD-SHELL', 'wget -qO /dev/null http://127.0.0.1/api/v1/health']
interval: 10s
timeout: 5s
retries: 12
start_period: 60s
Comment thread
ravikiranvm marked this conversation as resolved.
depends_on:
openops-tables:
condition: service_healthy
Expand All @@ -47,8 +53,44 @@ services:
volumes:
- ${HOST_AZURE_CONFIG_DIR:-openops_azure_cli_data}:/tmp/azure
- ${HOST_CLOUDSDK_CONFIG:-openops_gcloud_cli_data}:/tmp/gcloud
healthcheck:
test: ['CMD', 'curl', '-f', 'http://127.0.0.1:3000/v1/health']
interval: 10s
timeout: 5s
retries: 12
start_period: 60s
depends_on:
openops-app:
condition: service_healthy
openops-mcp:
image: openops.azurecr.io/openops-mcp:0.2.0
restart: unless-stopped
environment:
OPENOPS_API_URL: http://openops-app/api
OPENOPS_MCP_PROFILE: agent
OPENOPS_MCP_ISSUER: ${OPS_OAUTH_ISSUER_URL:-}
OPENOPS_MCP_RESOURCE_URL: ${OPS_MCP_RESOURCE_URL:-}
OPENOPS_MCP_CLIENT_SECRET: ${OPS_OAUTH_RS_CLIENT_SECRET:-}
LOG_LEVEL: ${OPS_LOG_LEVEL:-info}
LOGZIO_TOKEN: ${OPS_LOGZIO_TOKEN:-}
ENVIRONMENT: ${OPS_ENVIRONMENT_NAME:-docker-compose}
healthcheck:
test:
[
'CMD',
'python',
'-c',
"import urllib.request, sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:3020/.well-known/oauth-protected-resource/mcp', timeout=3).status == 200 else 1)",
]
interval: 20s
timeout: 5s
retries: 10
start_period: 30s
depends_on:
- openops-app
openops-app:
condition: service_healthy
profiles:
- mcp
openops-tables:
image: openops.azurecr.io/openops-tables:0.2.22
restart: unless-stopped
Expand Down
49 changes: 49 additions & 0 deletions deploy/docker-compose/nginx.gateway.routing.template
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,55 @@ location ~ ^/api/v1/webhooks/[^/]+/sync$ {
send_timeout ${NGINX_WEBHOOK_SYNC_TIMEOUT};
}

# MCP server for external agents (compose profile "mcp"). proxy_pass through a variable
# with Docker's DNS so nginx starts even when the profile is off. The /mcp prefix must not
# be stripped: the MCP server derives its OAuth resource identifier from it.
location /mcp {
resolver 127.0.0.11 valid=30s;
set $openops_mcp http://openops-mcp:3020;
proxy_pass $openops_mcp;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# --- MCP streaming-specific settings ---
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
proxy_read_timeout ${NGINX_WEBHOOK_SYNC_TIMEOUT};
}

location /.well-known/oauth-protected-resource {
resolver 127.0.0.11 valid=30s;
set $openops_mcp http://openops-mcp:3020;
proxy_pass $openops_mcp;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

# OAuth authorization server discovery (RFC 8414 and the OpenID path) for the API issuer
# ${OPS_PUBLIC_URL}/api. Without these the catch-all serves the frontend's index.html.
# The API registers both the bare path and the "/api"-suffixed variant.
location /.well-known/oauth-authorization-server {
proxy_pass http://openops-app/api/.well-known/oauth-authorization-server;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location /.well-known/openid-configuration {
proxy_pass http://openops-app/api/.well-known/openid-configuration;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location /.well-known/acme-challenge/ {
root /etc/nginx/tls/acme;
}
8 changes: 8 additions & 0 deletions docs/oauth.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,14 @@ the advertised resource and the actual audience disagree. And `OPS_MCP_RESOURCE_
equal `OPENOPS_MCP_RESOURCE_URL` there, exactly. Sharing a host is fine, since audiences are
compared with their path — `/api` and `/mcp` are distinct.

In the docker compose deployment (`deploy/docker-compose`) the two public values, issuer and
resource URL, derive from `OPS_PUBLIC_URL` in `.env.defaults`, while `OPENOPS_API_URL` is the
internal `http://openops-app/api`. The MCP server runs as the `openops-mcp` service behind the
`mcp` compose profile. The gateway nginx routes `/mcp` and `/.well-known/oauth-protected-resource`
to that service and the authorization-server discovery paths to the API; without the latter, the
catch-all would answer discovery requests with the frontend's `index.html`. Set
`OPS_OAUTH_ENABLED=true` and `COMPOSE_PROFILES=mcp` in `.env`, then `docker compose up -d`.

## Cleanup

An hourly system job (`oauth-cleanup-job.ts`) deletes expired authorization codes and pending
Expand Down
Loading