[{"data":1,"prerenderedAt":137},["ShallowReactive",2],{"doc:\u002Fdocs\u002Fproxies\u002Fcontainer":3},{"page":4,"toc":119,"updated":136},{"path":5,"title":6,"seoTitle":7,"description":8,"blocks":9},"\u002Fdocs\u002Fproxies\u002Fcontainer","Proxy container","Proxy Container for Docker, Kubernetes, HAProxy and Envoy","A small, dependency-free proxy container for any platform that can route one path to a container: Docker, Kubernetes, HAProxy, Envoy or a load balancer.",[10,13,15,20,82,85,87,106,108,110,113,115,117],{"type":11,"text":12},"p","A small Node service and a Dockerfile for any platform that runs containers: Docker, Kubernetes, or anything behind HAProxy, Envoy or a cloud load balancer that can route one path to a container. Your front proxy routes the path the dashboard issued to it, on your own host, and it forwards only the browser SDK's submissions to Fingerly. It is not an open proxy: the upstream is a constant in the code, and every other path and method is refused. Version 1.0.0.",{"type":11,"text":14},"The [Traefik](\u002Fdocs\u002Fproxies\u002Ftraefik) and [Google Cloud Run](\u002Fdocs\u002Fproxies\u002Fgoogle-cloud-run) templates run this same service.",{"type":16,"level":17,"text":18,"id":19},"heading",2,"Set up","set-up",{"type":21,"steps":22},"steps",[23,30,37,49,60,71],{"title":24,"blocks":25},"Create the proxy",[26,28],{"type":11,"text":27},"In the dashboard, open **Integration > Proxy keys** and choose **New proxy**. Pick the environment of the public key it will forward, and **Container** under **How it runs**. Copy the [proxy key](\u002Fdocs\u002Fproxy-integrations#proxy-keys) it shows once, and note the path it issued, such as `\u002Fk7x2qm4a`.",{"type":11,"text":29},"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.",{"title":31,"blocks":32},"Save the service",[33,35],{"type":11,"text":34},"Save `server.mjs` in an empty folder.",{"type":11,"text":36},"`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.",{"title":38,"blocks":39},"Save its Dockerfile",[40,42],{"type":11,"text":41},"Beside `server.mjs`. The image has no dependencies to install and runs as an unprivileged user, with a numeric ID so Kubernetes' `runAsNonRoot` can check it.",{"type":43,"samples":44},"code",[45],{"label":46,"lang":47,"code":48},"Dockerfile","dockerfile","FROM node:24-alpine\nWORKDIR \u002Fapp\nCOPY server.mjs .\u002F\nENV NODE_ENV=production PORT=8080\nUSER 1000:1000\nEXPOSE 8080\nCMD [\"node\", \"server.mjs\"]",{"title":50,"blocks":51},"Build and run it",[52,54],{"type":11,"text":53},"Take the key from your secret store, never a file in version control. `remote` is right only when visitors connect to the container directly; behind a proxy, choose the [address source](#how-the-visitor-s-address-is-trusted) that fits.",{"type":43,"samples":55},[56],{"label":57,"lang":58,"code":59},"Terminal","bash","docker build -t site-relay .\nexport FINGERLY_PROXY_KEY='fly_px_production_…'   # from your secret store\ndocker run -d --name site-relay --restart unless-stopped -p 8080:8080 \\\n  -e FINGERLY_PATH=\u002Fk7x2qm4a -e FINGERLY_PROXY_KEY \\\n  -e FINGERLY_CLIENT_IP_SOURCE=remote \\\n  site-relay",{"title":61,"blocks":62},"Route the path to it",[63,65],{"type":11,"text":64},"From the proxy or load balancer in front, send exactly this path on your site's host to the container, `POST` bodies included.",{"type":43,"samples":66},[67],{"label":68,"lang":69,"code":70},"Routing","text","POST https:\u002F\u002Fwww.example.com\u002Fk7x2qm4a  ->  http:\u002F\u002Fsite-relay:8080\u002Fk7x2qm4a",{"title":72,"blocks":73},"Point the browser SDK at the path",[74,80],{"type":43,"samples":75},[76],{"label":77,"lang":78,"code":79},"fingerly.ts","ts","import { load } from '@fingerly\u002Fweb-js'\n\nconst fingerly = await load({\n  apiKey: 'fly_pk_production_…',\n  endpoints: '\u002Fk7x2qm4a',\n})",{"type":11,"text":81},"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\u002F1.0.0`, with its requests over the last 30 days.",{"type":16,"level":17,"text":83,"id":84},"How the visitor's address is trusted","how-the-visitor-s-address-is-trusted",{"type":11,"text":86},"`FINGERLY_CLIENT_IP_SOURCE` says where the visitor's address comes from. The service sends it as `x-fingerly-client-ip`, and refuses to start when the value is not one of these:",{"type":88,"columns":89,"rows":93},"table",[90,91,92],"Value","Reads","Safe when",[94,98,102],[95,96,97],"`remote` (default)","The address of the connection.","Visitors connect to the container directly.",[99,100,101],"`header:\u003Cname>`","One header, such as `header:x-real-ip`.","The proxy in front sets that header on every request, overwriting what the visitor sent.",[103,104,105],"`xff:\u003Cn>`","The n-th `X-Forwarded-For` entry from the right.","Your own proxies append exactly n-1 entries after the visitor's address.",{"type":11,"text":107},"Only a header the front proxy overwrites is safe. A header it merely passes along, or the left-most `X-Forwarded-For` entry, is whatever the visitor chose to send. Behind one proxy that appends the connection address to `X-Forwarded-For`, `xff:1` is right; behind a load balancer and then a proxy that each append one, `xff:2`.",{"type":11,"text":109},"A repeated header, or a value that is not an IP address, is sent as empty, and the API then refuses the call with `422` and `invalid_visitor_metadata` rather than guessing.",{"type":16,"level":17,"text":111,"id":112},"What it sends and relays","what-it-sends-and-relays",{"type":11,"text":114},"Upstream, to `https:\u002F\u002Fapi.fingerly.io\u002Fapi\u002Fv1\u002Fingest` 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\u002F1.0.0`). Cookies, `Authorization` and any `x-fingerly-*` header a visitor sends never leave.",{"type":11,"text":116},"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. With `FINGERLY_PATH` unset, the service answers `404` to everything.",{"type":11,"text":118},"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.",[120,121,124,126,128,130,132,134,135],{"id":19,"text":18,"level":17},{"id":122,"text":24,"level":123},"step-create-the-proxy",3,{"id":125,"text":31,"level":123},"step-save-the-service",{"id":127,"text":38,"level":123},"step-save-its-dockerfile",{"id":129,"text":50,"level":123},"step-build-and-run-it",{"id":131,"text":61,"level":123},"step-route-the-path-to-it",{"id":133,"text":72,"level":123},"step-point-the-browser-sdk-at-the-path",{"id":84,"text":83,"level":17},{"id":112,"text":111,"level":17},"2026-09-25T18:31:31.000Z",1793630453201]