# Deploy to Kubernetes

This guide shows you how to containerize an app and run it on Kubernetes, connected to [Catalyst Cloud](https://docs.diagrid.io/operate/hosting/catalyst-cloud). It works the same for an AI agent, a workflow app and an app that uses the Dapr APIs.

Your code runs in a Kubernetes pod. There is no sidecar to install. The pod connects to Catalyst Cloud with the three environment variables from [Connect to Catalyst](https://docs.diagrid.io/develop/connect#configure-dapr-sdks-with-environment-variables), which you store as a Kubernetes Secret.

**Diagram: App pod connected to Catalyst Cloud.** The pod reads its Catalyst credentials from a Kubernetes Secret and connects to Catalyst Cloud through its App ID.

- **Kubernetes**: Secret (catalyst-credentials), App pod
- **Catalyst Cloud**: App ID, Dapr APIs (workflows, state, pub/sub)

- Secret (catalyst-credentials) → App pod
- App pod → App ID
- App ID → Dapr APIs (workflows, state, pub/sub)

## Prerequisites

- [Diagrid Catalyst account](https://catalyst.diagrid.io/)
- [Diagrid CLI](https://docs.diagrid.io/references/catalyst/catalyst-cli-intro)
- [Docker](https://docs.docker.com/get-docker/)
- A container registry (Docker Hub, ECR, GCR, ACR, etc.)
- A Kubernetes cluster with `kubectl` configured

## 1. Create a Catalyst project and app

If you don't already have a project and app from a previous tutorial:

```bash
diagrid project create my-project
diagrid app create my-app --project my-project
```

:::tip
Use descriptive names that match your application. You can reuse a project you already created in a previous tutorial. Create a new app for the Kubernetes deployment.
:::

## 2. Retrieve Catalyst credentials

You need three values to connect your pod to Catalyst: the project's HTTP endpoint, the project's gRPC endpoint, and the app's API token.

**Get the project endpoints:**

```bash
diagrid project get my-project -o json
```

Look for `status.endpoints.http.url` and `status.endpoints.grpc.url` in the output:

```json
{
  "status": {
    "endpoints": {
      "http": { "url": "https://http-prj123456.cloud.r1.diagrid.io:443" },
      "grpc": { "url": "https://grpc-prj123456.cloud.r1.diagrid.io:443" }
    }
  }
}
```

**Get the app API token:**

```bash
diagrid app get my-app --project my-project -o yaml
```

Look for `status.apiToken` in the output:

```yaml
status:
  apiToken: diagrid://v1/...
```

Save all three values. You'll store them as a Kubernetes Secret in a later step.

## 3. Write a Dockerfile

Create a `Dockerfile` in your project directory. Pick your language:

**Python**

```dockerfile
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "agent.py"]
```

Replace `agent.py` with your app's entry point.

Create a `requirements.txt` with your framework's dependencies:

**LangGraph**

```text
diagrid[langgraph]
fastapi
uvicorn
```

**CrewAI**

```text
diagrid[crewai]
fastapi
uvicorn
```

**OpenAI Agents**

```text
diagrid[openai_agents]
fastapi
uvicorn
```

**Pydantic AI**

```text
diagrid[pydantic_ai]
fastapi
uvicorn
```

**Google ADK**

```text
diagrid[adk]
fastapi
uvicorn
```

**Strands**

```text
diagrid[strands]
fastapi
uvicorn
```

**Dapr Agents**

```text
dapr-agents
fastapi
uvicorn
```

**Deep Agents**

```text
diagrid[deepagents]
fastapi
uvicorn
```

**Claude Agents**

```text
diagrid[claude_agents]
fastapi
uvicorn
```

**HolmesGPT**

```text
diagrid[holmesgpt]
fastapi
uvicorn
```

:::tip
If your app does not use the `diagrid` SDK (e.g. a plain access policy service invocation app), just list your direct dependencies: `fastapi`, `uvicorn`, `httpx`, etc.
:::

For a workflow or Dapr API app, list `dapr` and `dapr-ext-workflow` (workflows) plus your web framework instead.

**.NET**

```dockerfile
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app

FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "MyApp.dll"]
```

Replace `MyApp.dll` with your project's assembly name.

**Java**

```dockerfile
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /src
COPY . .
RUN mvn -q package -DskipTests

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /src/target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
```

This assumes your build produces one executable jar, as Spring Boot does.

**JavaScript**

```dockerfile
FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "index.js"]
```

For a TypeScript app, run it with `tsx`: `CMD ["npx", "tsx", "index.ts"]` (keep `tsx` in `dependencies`), or add a `tsc` build step.

**Go**

```dockerfile
FROM golang:1.26 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app .

FROM gcr.io/distroless/static
COPY --from=build /app /app
ENTRYPOINT ["/app"]
```

## 4. Build and push the image

Replace `your-registry` and `my-app` with your registry path and image name.

```bash
docker build -t your-registry/my-app:latest .
docker push your-registry/my-app:latest
```

## 5. Store credentials as a Kubernetes Secret

Create a Kubernetes Secret with the three Catalyst credentials you retrieved in step 2:

```bash
kubectl create secret generic catalyst-credentials \
  --from-literal=http-endpoint="https://http-prj123456.cloud.r1.diagrid.io:443" \
  --from-literal=grpc-endpoint="https://grpc-prj123456.cloud.r1.diagrid.io:443" \
  --from-literal=api-token="diagrid://v1/..."
```

:::tip
Replace the placeholder values with the real ones from `diagrid project get` and `diagrid app get`.
:::

## 6. Create a Kubernetes Deployment

Create `deployment.yaml`:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-app
          image: your-registry/my-app:latest
          ports:
            - containerPort: 8001
          env:
            - name: DAPR_HTTP_ENDPOINT
              valueFrom:
                secretKeyRef:
                  name: catalyst-credentials
                  key: http-endpoint
            - name: DAPR_GRPC_ENDPOINT
              valueFrom:
                secretKeyRef:
                  name: catalyst-credentials
                  key: grpc-endpoint
            - name: DAPR_API_TOKEN
              valueFrom:
                secretKeyRef:
                  name: catalyst-credentials
                  key: api-token
```

If your app needs other secrets, such as an LLM API key, add them to the Secret and reference them the same way:

```yaml
            - name: OPENAI_API_KEY
              valueFrom:
                secretKeyRef:
                  name: catalyst-credentials
                  key: openai-api-key
```

## 7. Let Catalyst reach your app (if it receives calls)

Skip this step if your app only calls Catalyst, for example a workflow worker or an agent that you start through the Catalyst APIs. Leave the app connection unset; see [When to leave the app connection unset](https://docs.diagrid.io/operate/project-operations/ids#when-to-leave-the-app-connection-unset). If people or other services call your app directly, still give it a [Kubernetes Service](https://kubernetes.io/docs/concepts/services-networking/service/).

If Catalyst calls your app, for pub/sub deliveries, service invocation, input bindings or health checks, the app needs an endpoint Catalyst can reach:

1. Expose the pod's port with a [Kubernetes Service](https://kubernetes.io/docs/concepts/services-networking/service/) and an ingress or load balancer that has a public address.

2. Point the app at that address, and set an application token so your app can check that the call comes from Catalyst:

   ```bash
   diagrid app update my-app --project my-project \
     --endpoint https://my-app.example.com \
     --endpoint-token "<a-secret-you-choose>"
   ```

3. In your app, reject requests whose `dapr-api-token` header doesn't match that token. See [Secure the application connection](https://docs.diagrid.io/develop/connect#secure-the-application-connection) for why this is not the same value as `DAPR_API_TOKEN`, and to allowlist the project's egress address.

## 8. Apply and verify

Deploy to your cluster:

```bash
kubectl apply -f deployment.yaml
```

Check that the pod is running:

```bash
kubectl get pods
```

Stream the logs to verify the app connected to Catalyst:

```bash
kubectl logs -f deployment/my-app
```

You should see the app start. A workflow app logs that its workflow worker connected to Catalyst. To check an app that receives calls, publish a test message or invoke it with [`diagrid call`](https://docs.diagrid.io/references/catalyst/cli-reference/call).

## Summary

In this guide, you:

- Created a Catalyst project and app for Kubernetes use
- Retrieved the HTTP endpoint, gRPC endpoint, and API token
- Containerized your app
- Stored Catalyst credentials as a Kubernetes Secret
- Deployed the app with the three environment variables your code reads from

## Next steps

- Watch your app's runs: [Operate workflows](https://docs.diagrid.io/operate/project-operations/workflows) or [Operate agents](https://docs.diagrid.io/operate/project-operations/agents).
- See metrics, API logs and the app topology in [Observability](https://docs.diagrid.io/operate/project-operations/observability).
- Decide which apps may call each other with [service invocation policies](https://docs.diagrid.io/operate/project-operations/policies/service-invocation).
- Scale out with [Horizontal Pod Autoscaling](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/).
- Keep traffic inside your own network: compare the [hosting options](https://docs.diagrid.io/operate/hosting).
