# Traefik proxy

> Route the one path the dashboard issued from Traefik to the small proxy container, with Docker labels, the file provider or a Kubernetes IngressRoute.

Last updated: 2026-09-25

Traefik's configuration alone cannot copy the visitor's address into a header: its headers middleware only sets fixed values. So this template routes the one path the dashboard issued to the [proxy container](https://docs.fingerly.io/docs/proxies/container), which does the forwarding. The router answers exactly that path, on your own host. Version 1.0.0, reported as `container/1.0.0` because the container runs it.

## Set up

These steps use Traefik's Docker provider. The [file provider](#file-provider-and-kubernetes) and Kubernetes follow them.

### Step 1: Create the proxy

In the dashboard, open **Integration > Proxy keys** and choose **New proxy**. Pick the environment of the public key it will forward, and **Traefik** under **How it runs**. Copy the [proxy key](https://docs.fingerly.io/docs/proxy-integrations#proxy-keys) it shows once, and note the path it issued, such as `/k7x2qm4a`.

The dashboard then prints the steps below, filled in with your proxy's own path and key. They stay under **Show setup** in the proxy's menu, without the key, which is never shown again.

### Step 2: Save the service

Save `server.mjs` as `site-relay/server.mjs` beside your compose file, and its Dockerfile beside it as `site-relay/Dockerfile`. The image has no dependencies and runs as an unprivileged user.

`server.mjs` is a small Node service with no dependencies: the built-in HTTP server and `fetch`. It reads `FINGERLY_PROXY_KEY`, `FINGERLY_PATH`, `FINGERLY_CLIENT_IP_SOURCE` and `PORT` (default `8080`), and refuses to start when the address source is not one it knows. The dashboard prints it; copy it from there.

```dockerfile site-relay/Dockerfile
FROM node:24-alpine
WORKDIR /app
COPY server.mjs ./
ENV NODE_ENV=production PORT=8080
USER 1000:1000
EXPOSE 8080
CMD ["node", "server.mjs"]
```

### Step 3: Add it to your compose project

In the project Traefik routes with its Docker provider, on a network Traefik can reach. If your other routers name `entrypoints` or `tls` in their labels, add the same to this one.

```yaml docker-compose.yml
services:
  site-relay:
    build: ./site-relay
    restart: unless-stopped
    environment:
      FINGERLY_PATH: /k7x2qm4a
      FINGERLY_CLIENT_IP_SOURCE: header:x-real-ip
      FINGERLY_PROXY_KEY: ${FINGERLY_PROXY_KEY:?set FINGERLY_PROXY_KEY}
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.site-relay.rule=Path(`/k7x2qm4a`)"
      - "traefik.http.routers.site-relay.priority=10000"
      - "traefik.http.services.site-relay.loadbalancer.server.port=8080"
```

### Step 4: Start it

Take the key from your secret store, or an `.env` file kept out of version control.

```bash Terminal
FINGERLY_PROXY_KEY='fly_px_production_…' docker compose up -d --build site-relay
```

### Step 5: Point the browser SDK at the path

```ts fingerly.ts
import { load } from '@fingerly/web-js'

const fingerly = await load({
  apiKey: 'fly_pk_production_…',
  endpoints: '/k7x2qm4a',
})
```

The SDK posts both of its calls to the path exactly as given. Once pages use it, the proxy's row in **Proxy keys** shows it last seen, running `container/1.0.0`, with its requests over the last 30 days.

> **Note:** The router has priority `10000`. Traefik otherwise ranks routers by the length of their rule, so a longer catch-all rule for your site would win over the one-path rule.

## File provider and Kubernetes

With the file provider, run the container as `site-relay` on a network Traefik can reach, and add this to your dynamic configuration:

```yaml dynamic.yml
http:
  routers:
    site-relay:
      rule: "Path(`/k7x2qm4a`)"
      priority: 10000
      service: site-relay
  services:
    site-relay:
      loadBalancer:
        servers:
          - url: "http://site-relay:8080"
```

On Kubernetes, push the image to your registry, create the key's Secret from your secret store, and apply a Deployment of it (`FINGERLY_PATH`, `FINGERLY_CLIENT_IP_SOURCE=header:x-real-ip`, and `FINGERLY_PROXY_KEY` from the Secret, running as non-root with a read-only root filesystem), a Service on port `8080`, and an `IngressRoute`:

```bash Secret
kubectl create secret generic site-relay --from-literal=proxy-key=fly_px_production_…
```

```yaml IngressRoute
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: site-relay
spec:
  routes:
    - match: Path(`/k7x2qm4a`)
      kind: Rule
      priority: 10000
      services:
        - name: site-relay
          port: 8080
```

The dashboard prints the whole manifest, the Deployment and the Service included.

## How the visitor's address is trusted

The container reads `X-Real-Ip` (`FINGERLY_CLIENT_IP_SOURCE=header:x-real-ip`). For a request from an address outside `forwardedHeaders.trustedIPs`, Traefik deletes every `X-Forwarded-*` and `X-Real-Ip` header the client sent and sets `X-Real-Ip` to the address of the connection it accepted, so a visitor cannot forge it. That is the default, with no trusted IPs configured.

- **Traefik behind a load balancer** listed in `forwardedHeaders.trustedIPs`: Traefik keeps the `X-Real-Ip` the load balancer sent, and only sets it when there is none. Keep `header:x-real-ip` if the load balancer sets `X-Real-Ip` itself on every request. If it only appends to `X-Forwarded-For`, use `xff:2` instead: the load balancer appends the visitor, then Traefik appends the load balancer.
- **Kubernetes**: Traefik sees the visitor's address only when its own Service preserves it, with `externalTrafficPolicy: Local` or the PROXY protocol from the load balancer. Otherwise every request carries a node's address.

> **Warning:** Never set `forwardedHeaders.insecure`. Traefik then keeps whatever the visitor sent.

## What it sends and relays

Upstream, to `https://api.fingerly.io/api/v1/ingest` and nowhere else, on headers built from scratch: `content-type`, `x-api-key`, `Idempotency-Key` when present, and the `x-fingerly-*` headers the proxy sets itself (the proxy key, the visitor's address, the page origin, the user agent and `x-fingerly-proxy-version: container/1.0.0`). Cookies, `Authorization` and any `x-fingerly-*` header a visitor sends never leave.

Other methods answer `405` with `Allow: POST`, and other paths `404`. Bodies over 1 MiB are refused with `413`, redirects are not followed, and a missing or malformed key answers `503` rather than forwarding anything. Traefik adds nothing to the response.

Back to the page: the API's status and body, with only `content-type`, `x-request-id`, `ratelimit-limit`, `ratelimit-remaining` and `retry-after`, plus `cache-control: no-store`. Nothing relayed names Fingerly.
