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

# Send data from Docker

> Run an OpenTelemetry Collector container beside your application containers, forward host and application telemetry, and verify it from the collector's own metrics.

One collector container, one config file. Your application containers export to
it over OTLP; it forwards everything to aiAxonIQ with your license key held in a
single place.

This is the shortest install that produces real, continuous telemetry rather
than a single test record.

<Info>
  **Before you start, you need two values.**

  1. **Your base endpoint** — shown on **Get Started** in the dashboard. These
     pages write it as `$OIQ_ENDPOINT`.
  2. **A license key** — created in **Settings → License Keys**, starting
     `oiq_`. Requires the Admin role. See
     [Create a license key](/get-started/license-keys). These pages write it as
     `$OIQ_LICENSE_KEY`.

  Export both before running anything below:

  ```bash theme={null}
  export OIQ_ENDPOINT="https://app.aiaxoniq.com/otlp"   # or your own
  export OIQ_LICENSE_KEY="oiq_..."
  ```
</Info>

You also need Docker with a running daemon — `docker ps` should succeed.

## Why a collector rather than exporting directly

Your application can export straight to aiAxonIQ, and for one container on a
laptop that is fine. On a host running several containers, the collector earns
its place for three reasons: the license key stops being duplicated into every
container's environment, host and container metrics become available at all
(an instrumented app cannot see the machine it runs on), and batching and retry
are configured once instead of per service.

## 1. Write the config

Save this as `config.yaml`. It receives OTLP on `4317`/`4318`, scrapes host
metrics, and exports to aiAxonIQ.

```yaml config.yaml theme={null}
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318
  hostmetrics:
    collection_interval: 30s
    scrapers:
      cpu:
      memory:
      disk:
      filesystem:
      network:
      load:

processors:
  # Order matters and is not alphabetical. memory_limiter can only shed load it
  # sees before anything has buffered it, so it goes first; batch should group
  # records after every other processor has finished changing them, so it goes
  # last. A pipeline that batches first and limits afterwards will still run out
  # of memory under exactly the load the limiter was added for.
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
    spike_limit_percentage: 25
  resourcedetection:
    detectors: [env, system]
    system:
      hostname_sources: [os]
  batch:
    timeout: 5s
    send_batch_size: 1000

exporters:
  otlphttp/aiaxoniq:
    endpoint: ${env:OIQ_ENDPOINT}
    headers:
      X-License-Key: ${env:OIQ_LICENSE_KEY}
    compression: gzip

service:
  telemetry:
    metrics:
      readers:
        - pull:
            exporter:
              prometheus:
                host: 0.0.0.0
                port: 8888
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, resourcedetection, batch]
      exporters: [otlphttp/aiaxoniq]
    metrics:
      receivers: [otlp, hostmetrics]
      processors: [memory_limiter, resourcedetection, batch]
      exporters: [otlphttp/aiaxoniq]
    logs:
      receivers: [otlp]
      processors: [memory_limiter, resourcedetection, batch]
      exporters: [otlphttp/aiaxoniq]
```

## 2. Run the collector

```bash theme={null}
docker run -d --name aiaxoniq-collector \
  --restart unless-stopped \
  -v "$PWD/config.yaml:/etc/otelcol-contrib/config.yaml:ro" \
  -e OIQ_ENDPOINT="$OIQ_ENDPOINT" \
  -e OIQ_LICENSE_KEY="$OIQ_LICENSE_KEY" \
  -p 4317:4317 \
  -p 4318:4318 \
  -p 8888:8888 \
  otel/opentelemetry-collector-contrib:latest
```

| Port   | Purpose                                         |
| :----- | :---------------------------------------------- |
| `4317` | OTLP/gRPC in, from your applications            |
| `4318` | OTLP/HTTP in, from your applications            |
| `8888` | The collector's own metrics — how you verify it |

<Tip>
  Pin a version rather than using `latest` for anything you intend to keep. The
  collector's configuration schema does change between releases, and `latest`
  turns that into a surprise on restart.
</Tip>

## 3. Confirm it started

```bash theme={null}
docker logs aiaxoniq-collector 2>&1 | head -20
```

**Expected output** — the line that matters is the last one:

```text theme={null}
info  service@v0.115.0/service.go:166   Setting up own telemetry...
info  telemetry/metrics.go:70           Serving metrics  {"address": "0.0.0.0:8888"}
info  memorylimiter@v0.115.0/…          Memory limiter configured  {"limit_mib": 12534}
info  service@v0.115.0/service.go:238   Starting otelcol-contrib...  {"Version": "0.115.0"}
info  otlpreceiver@v0.115.0/otlp.go:112 Starting GRPC server  {"endpoint": "0.0.0.0:4317"}
info  otlpreceiver@v0.115.0/otlp.go:169 Starting HTTP server  {"endpoint": "0.0.0.0:4318"}
info  service@v0.115.0/service.go:261   Everything is ready. Begin running and processing data.
```

`Everything is ready. Begin running and processing data.` is the success signal.

You will also see two warnings, and both are expected:

<AccordionGroup>
  <Accordion title="'Using the 0.0.0.0 address exposes this server to every network interface'" icon="shield">
    Correct and intentional here. The collector must accept connections from
    other containers, which it cannot do bound to loopback.

    On a host reachable from an untrusted network, do not publish `4317`/`4318`
    with `-p`. Put the collector and your applications on a shared Docker
    network instead and let them reach it by container name — nothing needs to
    be published to the host at all.
  </Accordion>

  <Accordion title="'No root_path config set when running in docker environment'" icon="triangle-exclamation">
    This one changes what your data means, so it is worth acting on.

    The `hostmetrics` receiver inside a container sees the **container's**
    filesystem, not the host's. Disk and filesystem metrics will describe the
    collector container — which is small, idle and not what you wanted to
    monitor.

    To report real host metrics, mount the host root read-only and tell the
    receiver where it is:

    ```bash theme={null}
    -v /:/hostfs:ro
    ```

    ```yaml theme={null}
    receivers:
      hostmetrics:
        root_path: /hostfs
    ```
  </Accordion>
</AccordionGroup>

## 4. Point your applications at it

Applications on the same host export to the collector rather than to aiAxonIQ.
They carry **no license key** — that is the point.

<CodeGroup>
  ```bash Same Docker network theme={null}
  # Recommended: no ports published to the host at all.
  docker network create observability

  docker network connect observability aiaxoniq-collector

  docker run -d --name checkout \
    --network observability \
    -e OTEL_EXPORTER_OTLP_ENDPOINT="http://aiaxoniq-collector:4318" \
    -e OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" \
    -e OTEL_SERVICE_NAME="checkout" \
    your-image:tag
  ```

  ```bash Via the host theme={null}
  # Works when the collector published 4318 with -p.
  docker run -d --name checkout \
    -e OTEL_EXPORTER_OTLP_ENDPOINT="http://host.docker.internal:4318" \
    -e OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" \
    -e OTEL_SERVICE_NAME="checkout" \
    your-image:tag
  ```
</CodeGroup>

<Warning>
  `OTEL_EXPORTER_OTLP_ENDPOINT` is a **base** URL. The SDK appends `/v1/traces`
  itself, so a value ending in `/v1/traces` produces requests to
  `/v1/traces/v1/traces` — a 404 that reads like a wrong endpoint and sends
  people to change the one part that was correct.
</Warning>

## Verify

The collector publishes its own metrics on port `8888`. Three counters separate
the two things that can be wrong:

```bash theme={null}
curl -s localhost:8888/metrics | grep -E 'otelcol_(receiver_accepted|exporter_sent|exporter_send_failed)'
```

| Counter                          | Rising means                               |
| :------------------------------- | :----------------------------------------- |
| `otelcol_receiver_accepted_*`    | Your application is reaching the collector |
| `otelcol_exporter_sent_*`        | The collector is reaching aiAxonIQ         |
| `otelcol_exporter_send_failed_*` | It is trying and being refused             |

Accepted rising while sent stays flat is the classic signature of a wrong
endpoint or a missing key in the exporter block. It is worth checking before
anything else, because from inside your application everything looks fine.

**Expected output** shortly after start, with host metrics flowing:

```text theme={null}
otelcol_receiver_accepted_metric_points{receiver="hostmetrics"} 312
otelcol_exporter_sent_metric_points{exporter="otlphttp/aiaxoniq"} 312
otelcol_exporter_send_failed_metric_points{exporter="otlphttp/aiaxoniq"} 0
```

`send_failed` at zero and `sent` climbing is a working install.

Then confirm it landed: open **Logs** or **Infrastructure**, set the range to
the last 15 minutes, and look for your host. Full detail in
[Verify your data arrived](/get-started/verify-data).

## Troubleshooting

<AccordionGroup>
  <Accordion title="The collector starts, then logs 'Exporting failed. Will retry'" icon="arrows-rotate">
    Read the `error` field on that line — it names the cause exactly.

    ```text theme={null}
    Exporting failed. Will retry the request after interval.
      {"kind": "exporter", "name": "otlphttp/aiaxoniq",
       "error": "failed to make an HTTP request: Post \"…/v1/metrics\":
                 dial tcp: lookup … : no such host", "interval": "5.2s"}
    ```

    * `no such host` — the endpoint hostname does not resolve. Check
      `$OIQ_ENDPOINT` against the value on **Get Started**.
    * `connection refused` — the host resolves but nothing is listening on that
      port.
    * `401 Unauthorized` — the key is missing, malformed or revoked.
    * `404` — the endpoint already ends in `/v1/…`. It must be the **base** URL;
      the collector appends the signal path itself.

    The collector retries with backoff and does not drop data while it retries,
    so a transient failure here is not a loss.
  </Accordion>

  <Accordion title="Nothing at all in the logs after 'Everything is ready'" icon="ear-listen">
    The collector is running and receiving nothing. That is an application-side
    problem, not a collector one — your services are not exporting to it.

    Check that your application's `OTEL_EXPORTER_OTLP_ENDPOINT` points at the
    collector's OTLP port (`4318` for HTTP, `4317` for gRPC), not at aiAxonIQ.
  </Accordion>

  <Accordion title="401 with a key you know is correct" icon="key">
    Two causes that are not about the key's value:

    * **A trailing newline.** A key read from a file created by a shell heredoc,
      or a Kubernetes Secret made with `--from-file`, carries the newline as
      part of the value. Use `--from-literal`, or `printf` rather than `echo`.
    * **A validation outage.** If the receiver cannot reach the service that
      validates keys it fails closed and returns the same `401`. A sudden `401`
      across every service at once, with a key you have not changed, is far more
      likely to be this. Check `$OIQ_ENDPOINT/health` first.
  </Accordion>

  <Accordion title="Config changes appear to do nothing" icon="file-pen">
    The collector reads its configuration only at startup. Restart it after any
    edit, and confirm the file you edited is the one mounted into the process —
    a bind mount pointing at a path that does not exist silently yields the
    image's default config rather than an error.
  </Accordion>
</AccordionGroup>

<AccordionGroup>
  <Accordion title="The container exits immediately" icon="power-off">
    Almost always a config error — the collector refuses to start on an invalid
    configuration rather than running degraded.

    ```bash theme={null}
    docker logs aiaxoniq-collector
    ```

    Validate before running, which reports the problem without starting
    anything:

    ```bash theme={null}
    docker run --rm \
      -v "$PWD/config.yaml:/etc/otelcol-contrib/config.yaml:ro" \
      otel/opentelemetry-collector-contrib:latest \
      validate --config=/etc/otelcol-contrib/config.yaml
    ```

    Exit code `0` and no output means the config is valid.
  </Accordion>

  <Accordion title="Disk and filesystem metrics look wrong or tiny" icon="hard-drive">
    You are seeing the collector container's own filesystem. Mount the host root
    and set `root_path` — see the `root_path` warning above.
  </Accordion>

  <Accordion title="Applications cannot reach the collector" icon="plug">
    Confirm which name they should use. On a shared Docker network it is the
    container name (`aiaxoniq-collector`); from the host it is `localhost`; from
    inside another container reaching the host it is `host.docker.internal`,
    which does not exist on Linux without `--add-host`.

    Test from inside the application container rather than from your shell:

    ```bash theme={null}
    docker exec checkout curl -sv http://aiaxoniq-collector:4318/v1/traces
    ```
  </Accordion>
</AccordionGroup>

## Next

<CardGroup cols={3}>
  <Card title="Docker Compose" icon="layer-group" href="/send-data/platforms/docker-compose">
    The same collector as a service in your stack.
  </Card>

  <Card title="Instrument your application" icon="code" href="/send-data/otel/zero-code">
    Zero-code setup per language.
  </Card>

  <Card title="Verify your data" icon="circle-check" href="/get-started/verify-data">
    Confirm it landed.
  </Card>
</CardGroup>
