> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zeroruntime.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Self Hosting

> Run your agent worker on your own Docker host, VM, or Kubernetes cluster. The Zero Runtime runtime still handles real-time media.

Self-hosting means you run the **worker** (your agent code) on your own infrastructure
instead of Zero Runtime Cloud. The runtime still handles transport, GPUs, STT/LLM/TTS
orchestration, and turn-taking, so there's no media infrastructure for you to operate.

When your process calls `zeroruntime.serve(...)`, it opens an outbound connection to the
runtime and registers your agent under the id in `ZERORUNTIME_AGENT_ID`. The runtime then
dispatches sessions for that id to every registered worker and load-balances across them.

<Note>
  The CLI commands under [Operate](/deployments/operate/overview) (`up`, `version`, `secrets`,
  `logs`, `down`) manage workers on Zero Runtime Cloud. They don't apply here; use your own
  platform for rollouts, secrets, scaling, and logs.
</Note>

## Prerequisites

* An agent project whose `main.py` calls `zeroruntime.serve(...)`. Every
  [quickstart](https://github.com/ZeroRuntimeAI/zeroruntime-python-examples/blob/main/getting_started/cascade_basic.py) example does.
* Docker on the machine that builds the image (and on the host that runs it, for Docker).
* For Kubernetes: a cluster, `kubectl` configured for it, and a container registry the
  cluster can pull from.
* Your auth token from the [Zero Runtime dashboard](https://app.zeroruntime.ai/).

## Choose an agent ID

On Zero Runtime Cloud, the platform assigns the agent ID for you. When you self-host,
**you choose it** and set it as `ZERORUNTIME_AGENT_ID`. The worker registers under this ID,
and everything that reaches your agent targets it: `zeroruntime invoke --agent-id`, the
dispatch API, and telephony routing rules.

Pick an ID that is:

* **Unique**: don't reuse it for a different agent. Workers that share an ID receive each
  other's sessions.
* **Stable**: a fixed name like `support-agent`, not a random value generated per run or per
  container. Every replica of the same agent uses the same ID; that's how the runtime
  load-balances across them.

<Warning>
  If `ZERORUNTIME_AGENT_ID` is unset, the worker registers as `zeroruntime_agent`. Sessions and
  calls aimed at your real ID then never reach it. Always set it explicitly.
</Warning>

## Configure the environment

The worker reads its agent ID, connection details, and provider keys from environment
variables. Create a `.env` file in your project folder. See
[Authentication](/authentication) for the full list.

```bash .env highlight={1} theme={null}
ZERORUNTIME_AGENT_ID=support-agent
ZERORUNTIME_TARGET=us2.zeroruntime.ai:443
ZERORUNTIME_AUTH_TOKEN=<token>

# Configure an API key for each provider you use. Not required when using inference.
DEEPGRAM_API_KEY=<key>
GOOGLE_API_KEY=<key>
CARTESIA_API_KEY=<key>
```

<Warning>
  Keep comments on their own lines. `docker run --env-file` and `kubectl --from-env-file` don't
  strip trailing `# comments`, so they'd become part of the value.
</Warning>

Pass these at runtime. Don't bake them into the image: add `.env` to `.dockerignore`.

```text .dockerignore theme={null}
.env
.venv/
__pycache__/
```

## Remove the local test invoke

For local testing, use the `on_ready` callback in `serve()` to start a playground session when the worker is ready. Remove it in production to avoid automatically creating a new room for every worker. In production, start sessions via `zeroruntime invoke` CLI, the dispatch API, or telephony routing rules.

<CodeGroup>
  ```python main.py theme={null}
  def invoke_agent():  # [!code --]
      zeroruntime.invoke(room=Room(playground=True))  # [!code --]

  if __name__ == "__main__":
      zeroruntime.serve(MyVoiceAgent, on_ready=invoke_agent)  # [!code --]
      zeroruntime.serve(MyVoiceAgent)  # [!code ++]
  ```

  ```typescript agent.ts theme={null}
  await zeroruntime.serve(MyVoiceAgent, { // [!code --]
    on_ready: () => zeroruntime.invoke({ room: Room({ playground: true }) }), // [!code --]
  }); // [!code --]
  await zeroruntime.serve(MyVoiceAgent, {}); // [!code ++]
  ```
</CodeGroup>

## Build the image

Use the `Dockerfile` that `zeroruntime init` generates, or write your own. It should install
`requirements.txt` and run `python main.py`:

```dockerfile Dockerfile theme={null}
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]
```

Then build it from your project folder:

```bash theme={null}
docker build -t my-agent:v0.0 .
```

## Run the worker

<Tabs>
  <Tab title="Docker">
    <Steps>
      <Step title="Start the container">
        Run the worker in the background, load your `.env`, and restart it if it exits:

        ```bash theme={null}
        docker run -d --name my-agent \
          --restart unless-stopped \
          --env-file .env \
          -p 8081:8081 \
          my-agent:v0.0
        ```

        The worker only dials out to the runtime. `-p 8081:8081` is optional and only exposes
        the health endpoint.
      </Step>

      <Step title="Confirm it registered">
        Check the logs for a successful connection and registration under your agent ID:

        ```bash theme={null}
        docker logs -f my-agent
        ```

        Then check the health endpoint:

        ```bash theme={null}
        curl http://localhost:8081/health
        ```
      </Step>

      <Step title="Add more workers (optional)">
        To handle more concurrent sessions, start more containers with the **same**
        `.env`. Use a unique container name and host port for each:

        ```bash theme={null}
        docker run -d --name my-agent-2 --restart unless-stopped \
          --env-file .env -p 8082:8081 my-agent:v0.0
        ```

        Both workers register under the same agent ID, and the runtime balances sessions
        across them.
      </Step>

      <Step title="Ship an update">
        Build a new tag, then replace the running container:

        ```bash theme={null}
        docker build -t my-agent:v0.1 .
        docker stop my-agent && docker rm my-agent
        docker run -d --name my-agent --restart unless-stopped \
          --env-file .env -p 8081:8081 my-agent:v0.1
        ```

        `docker stop` sends `SIGTERM` and waits 10 seconds by default. Raise it with
        `docker stop -t <seconds>` if your sessions need longer to wrap up.
      </Step>
    </Steps>
  </Tab>

  <Tab title="Kubernetes">
    <Steps>
      <Step title="Push the image to a registry">
        The cluster must be able to pull the image. Tag it for your registry and push it:

        ```bash theme={null}
        docker tag my-agent:v0.0 registry.example.com/my-agent:v0.0
        docker push registry.example.com/my-agent:v0.0
        ```

        If your registry is private, create an
        [image pull secret](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/)
        and reference it under `imagePullSecrets` in the Deployment.
      </Step>

      <Step title="Create a namespace">
        ```bash theme={null}
        kubectl create namespace zeroruntime
        ```
      </Step>

      <Step title="Store your environment as a Secret">
        Create a Secret from the same `.env` file, including `ZERORUNTIME_AGENT_ID`:

        ```bash theme={null}
        kubectl create secret generic my-agent-secrets \
          --namespace zeroruntime \
          --from-env-file=.env
        ```
      </Step>

      <Step title="Create the Deployment">
        The worker is stateless, so a plain `Deployment` works. Every replica loads the same
        Secret, so all of them register under the same agent ID:

        ```yaml deployment.yaml theme={null}
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: my-agent
          namespace: zeroruntime
        spec:
          replicas: 3
          selector:
            matchLabels: { app: my-agent }
          template:
            metadata:
              labels: { app: my-agent }
            spec:
              terminationGracePeriodSeconds: 60
              containers:
                - name: worker
                  image: registry.example.com/my-agent:v0.0
                  envFrom:
                    - secretRef:
                        name: my-agent-secrets
                  ports:
                    - containerPort: 8081
                      name: health
                  livenessProbe:
                    httpGet: { path: /health, port: 8081 }
                    initialDelaySeconds: 10
                    periodSeconds: 15
        ```

        You don't need a `Service` or `Ingress`: the worker only dials out.
      </Step>

      <Step title="Apply it and confirm the rollout">
        ```bash theme={null}
        kubectl apply -f deployment.yaml
        kubectl rollout status deployment/my-agent -n zeroruntime
        kubectl get pods -n zeroruntime -l app=my-agent
        ```

        Check that the workers connected and registered:

        ```bash theme={null}
        kubectl logs -n zeroruntime -l app=my-agent --tail=50
        ```
      </Step>

      <Step title="Scale">
        Change the replica count to match your traffic:

        ```bash theme={null}
        kubectl scale deployment/my-agent -n zeroruntime --replicas=5
        ```
      </Step>

      <Step title="Ship an update">
        Push a new tag and point the Deployment at it. Kubernetes rolls the pods one by one:

        ```bash theme={null}
        docker build -t registry.example.com/my-agent:v0.1 .
        docker push registry.example.com/my-agent:v0.1
        kubectl set image deployment/my-agent \
          worker=registry.example.com/my-agent:v0.1 -n zeroruntime
        kubectl rollout status deployment/my-agent -n zeroruntime
        ```

        To change credentials, update the Secret and restart the pods so they pick up the
        new values:

        ```bash theme={null}
        kubectl create secret generic my-agent-secrets -n zeroruntime \
          --from-env-file=.env --dry-run=client -o yaml | kubectl apply -f -
        kubectl rollout restart deployment/my-agent -n zeroruntime
        ```
      </Step>
    </Steps>
  </Tab>
</Tabs>

## Start a session

Self-hosted workers receive sessions the same way cloud workers do. Start one with the CLI,
passing your `ZERORUNTIME_AGENT_ID`:

```bash theme={null}
zeroruntime invoke --agent-id support-agent
```

The CLI prints a playground link you can open to talk to your agent.

## Connect telephony

To take or place phone calls, configure gateways and a routing rule that point at your
agent. Use your `ZERORUNTIME_AGENT_ID` wherever an agent ID is required; it must match
**exactly**, or calls never reach your workers.

<AccordionGroup>
  <Accordion title="Set up inbound calls" icon="phone-arrow-down-left">
    **1. Create an inbound gateway**: the entry point for incoming calls.

    <Tabs>
      <Tab title="Dashboard">
        1. Copy the **Inbound URL** from the Zero Runtime dashboard.
        2. Go to **Twilio** and create a new SIP Trunk.
        3. Go to the **Origination** section, paste the Inbound URL there, and save it.

        <Frame>
          <video autoPlay loop muted playsInline controls className="w-full aspect-video rounded-xl" src="https://assets.videosdk.live/images/setup.mp4" />
        </Frame>
      </Tab>

      <Tab title="API">
        ```bash theme={null}
        curl --request POST \
          --url https://api.videosdk.live/v2/sip/inbound-gateways \
          --header 'Authorization: YOUR_ZERORUNTIME_AUTH_TOKEN' \
          --header 'Content-Type: application/json' \
          --data '{ "name": "My Inbound Gateway", "numbers": ["+1234567890"] }'
        ```

        Then set the **origination URI** of your Twilio SIP trunk to the inbound gateway hostname (shown in the dashboard), e.g. `sip:9WXXXXXXX.sip.videosdk.live`.
      </Tab>
    </Tabs>

    **2. Create a routing rule**: connects the inbound gateway to your self-hosted agent.

    <Tabs>
      <Tab title="Dashboard">
        1. From your inbound gateway, click **Configure rule**, then **Create new routing rule**.
        2. Enter a **Routing Rule Name** and select **API Key** authentication.
        3. Choose the **Call Direction** (Inbound).
        4. Enter the **Phone Number** and select a **Room Type**.
        5. Enter your `ZERORUNTIME_AGENT_ID` as the **Agent ID** and click **Save**.

        <Frame>
          <video autoPlay loop muted playsInline controls className="w-full aspect-video rounded-xl" src="https://assets.videosdk.live/images/routing-rules.mp4" />
        </Frame>
      </Tab>

      <Tab title="API">
        ```bash theme={null}
        curl --request POST \
          --url https://api.videosdk.live/v2/sip/routing-rule \
          --header 'Authorization: YOUR_ZERORUNTIME_AUTH_TOKEN' \
          --header 'Content-Type: application/json' \
          --data '{
            "gatewayId": "gateway_in_123456789",
            "name": "Support Line Rule",
            "numbers": ["+1234567890"],
            "agentId": "support-agent"
          }'
        ```
      </Tab>
    </Tabs>

    **3. Test it**: with your workers running, dial your provider's phone number from any phone. The agent greets you as soon as the call connects.

    <Tip>
      Using a different SIP provider? Follow its inbound setup:
      [Telnyx](/telephony/integrations/telnyx),
      [Vonage](/telephony/integrations/vonage#configuring-inbound-calling), or
      [Plivo](/telephony/integrations/plivo).
    </Tip>
  </Accordion>

  <Accordion title="Set up outbound calls" icon="phone-arrow-up-right">
    **1. Create an outbound gateway**: lets the agent dial out to the PSTN.

    <Tabs>
      <Tab title="Dashboard">
        1. In Twilio, open your SIP Trunk and go to the **Termination** section. Create a **Termination URI** and add username/password credentials.
        2. Back in the Zero Runtime Dashboard, open the outbound gateway configuration.
        3. Paste the Twilio **Termination URI** as the address, add the same **username/password** credentials, and save.

        <Frame>
          <video autoPlay loop muted playsInline controls className="w-full aspect-video rounded-xl" src="https://assets.videosdk.live/images/outbound-gateway.mp4" />
        </Frame>
      </Tab>

      <Tab title="API">
        ```bash theme={null}
        curl --request POST \
          --url https://api.videosdk.live/v2/sip/outbound-gateways \
          --header 'Authorization: YOUR_ZERORUNTIME_AUTH_TOKEN' \
          --header 'Content-Type: application/json' \
          --data '{
            "name": "My Outbound Gateway",
            "numbers": ["+12065551234"],
            "address": "sip.myprovider.com",
            "transport": "udp",
            "auth": { "username": "your-username", "password": "your-password" }
          }'
        ```

        Set `address` and `auth` to match your Twilio **Termination URI** and credentials so outbound calls reach the PSTN.
      </Tab>
    </Tabs>

    **2. Place a call**: with your workers running, trigger a call from the agent. Replace `YOUR_ZERORUNTIME_AUTH_TOKEN` and `routingRuleId` with your own.

    ```bash theme={null}
    curl --request POST \
      --url https://api.videosdk.live/v2/sip/call \
      --header 'Authorization: YOUR_ZERORUNTIME_AUTH_TOKEN' \
      --header 'Content-Type: application/json' \
      --data '{
        "sipCallFrom": "+14155550100",
        "sipCallTo": "+14155550199",
        "routingRuleId": "rr_2554md"
      }'
    ```

    <Tip>
      For lower latency, run your workers in the same region as your SIP provider.
    </Tip>

    <Tip>
      Using a different SIP provider? Follow its outbound setup:
      [Telnyx](/telephony/integrations/telnyx),
      [Vonage](/telephony/integrations/vonage#configuring-outbound-calling), or
      [Plivo](/telephony/integrations/plivo).
    </Tip>
  </Accordion>
</AccordionGroup>

## Operating self-hosted workers

* **Capacity:** each worker accepts up to `capacity` concurrent sessions (default `10`, set in
  `zeroruntime.serve(...)` or with `ZERORUNTIME_MAX_CONCURRENT_SESSIONS`). Add workers or
  replicas when you need more.
* **Health:** each worker serves `GET /health` on port `8081` for liveness probes.
* **Rolling updates:** build a new image tag and roll it out with your platform's usual
  deploy process.
* **Credentials:** rotate tokens and provider keys in your secret manager, then restart the
  workers so they pick up the new values.
* **Logs:** workers log to stdout and stderr. Collect them with your existing logging stack.

## Troubleshooting

* **Sessions or calls never arrive:** the `agentId` in `invoke`, dispatch, or your routing
  rule doesn't exactly match `ZERORUNTIME_AGENT_ID`, or the variable is unset and the worker
  registered as `zeroruntime_agent`. Check the worker logs for the ID it registered under.
* **Worker can't connect:** check `ZERORUNTIME_TARGET` and `ZERORUNTIME_AUTH_TOKEN`, and that
  outbound traffic to the runtime on port `443` is allowed.
* **Pods restart in a loop:** check `kubectl logs` for a startup error, and raise the
  probe's `initialDelaySeconds` if the worker takes longer to start.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.