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, 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 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 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.mjsassite-relay/server.mjsbeside your compose file, and its Dockerfile beside it assite-relay/Dockerfile. The image has no dependencies and runs as an unprivileged user.server.mjsis a small Node service with no dependencies: the built-in HTTP server andfetch. It readsFINGERLY_PROXY_KEY,FINGERLY_PATH,FINGERLY_CLIENT_IP_SOURCEandPORT(default8080), and refuses to start when the address source is not one it knows. The dashboard prints it; copy it from there.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
entrypointsortlsin their labels, add the same to this one.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
.envfile kept out of version control.FINGERLY_PROXY_KEY='fly_px_production_…' docker compose up -d --build site-relayStep 5: Point the browser SDK at the path
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.
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:
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:
kubectl create secret generic site-relay --from-literal=proxy-key=fly_px_production_…
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 theX-Real-Ipthe load balancer sent, and only sets it when there is none. Keepheader:x-real-ipif the load balancer setsX-Real-Ipitself on every request. If it only appends toX-Forwarded-For, usexff:2instead: 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: Localor the PROXY protocol from the load balancer. Otherwise every request carries a node's address.
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.