---
title: "Collector topology: one root, many branches"
description: "Loggerhead runs one OTLP receiver, the root, inside the app. Every other collector is a branch that forwards into it."
source: "https://getloggerhead.com/docs/collectors/topology-root-and-branches/"
---

# Collector topology: one root, many branches

> Loggerhead runs one OTLP receiver, the root, inside the app. Every other collector is a branch that forwards into it.

Loggerhead runs one OTLP receiver, the root, inside the app. Every other collector is a branch that forwards into it. This page shows how to connect a branch and how to keep records attributed.

## Why one root

The app is the only process that writes to the database. The root receiver lives in that process and binds to localhost. A branch never touches the database. It speaks plain OTLP to the root, so a standard upstream OpenTelemetry Collector, an SDK exporter, or otel-cli can all be a branch.

```text
 Docker host ─┐
 Kubernetes ──┼── OTLP ──▶  Loggerhead ROOT receiver  ──▶  DuckDB (one writer)
 Other hosts ─┘             127.0.0.1:<effective port>
```

## Two rules for every branch

:::note[Use the effective port, not 4317]
The root prefers 4317 and 4318. When another program holds them, the root moves along a fixed ladder. The Collectors page reports the port the root uses now, and the MCP tool `get_receiver_info` reports the same port to your AI coding tool.
:::

### Use the effective port

Read the port from the Collectors page, or ask your AI coding tool for it, and put that port in every branch config. Do not hard-code 4317. See [Ports and the effective port](/docs/getting-started/ports-and-the-effective-port/).

### Stamp the collector ID

Stamp `loggerhead.collector.id` with the resource processor. The root attributes each record to its branch, and the Collectors page shows each branch with its own health and throughput.

### Stamp the environment

Stamp `deployment.environment.name` in the same resource processor when the branch runs in staging or production. The value picks the environment inside the workspace: a record tagged `staging` lands in the Staging database and nowhere else. A branch with no tag writes to Local. See [Workspaces and environments](/docs/getting-started/workspaces-and-environments/).

## Run a Docker branch

Save this file as `branch.yaml` and start the upstream collector image with it. The container reaches the root on the host through `host.docker.internal`.

```yaml title="branch.yaml"
receivers:
  otlp:
    protocols:
      grpc: { endpoint: 0.0.0.0:4317 }

processors:
  resource:
    attributes:
      - key: loggerhead.collector.id
        value: docker-mac
        action: upsert
      - key: deployment.environment.name
        value: staging                    # the tag; omit for Local
        action: upsert

exporters:
  otlp:
    endpoint: host.docker.internal:4317   # the effective port
    tls: { insecure: true }

service:
  pipelines:
    logs:    { receivers: [otlp], processors: [resource], exporters: [otlp] }
    traces:  { receivers: [otlp], processors: [resource], exporters: [otlp] }
    metrics: { receivers: [otlp], processors: [resource], exporters: [otlp] }
```

## Address table

Where the root is reachable from, by the kind of branch that sends to it.

| Branch runs on | Address to use | Notes |
| --- | --- | --- |
| The same Mac | `127.0.0.1:<port>` | Loopback. Fastest path. |
| A container on this Mac | `host.docker.internal:<port>` | Docker Desktop and OrbStack proxy it. |
| A Kubernetes cluster | A tunnel to `127.0.0.1:<port>` | The root binds to loopback only. |
| Another machine | Not supported yet | Run a branch on that machine and tunnel. |

## Known limits

- The root binds to loopback only. A branch on another machine needs a tunnel.
- One root per Mac. Two apps cannot share a database.
