Behind your own proxy
Put your own load balancer, reverse proxy or CDN in front of GoRunner.
This page is for self-hosted installations. On GoRunner Cloud, GoRunner runs this for you.
Your proxy holds the certificate and passes every request to GoRunner on a local port. No Caddy runs. On Kubernetes, your ingress plays this part. See The ingress.
1.Run the setup in own-proxy mode
In the install folder, give the address people open and where your proxy reaches GoRunner.
--behind-proxy- The https address your proxy serves.
--app-port- Where your proxy connects.
127.0.0.1:8080, the default, suits a proxy on this server. Never an address the internet can reach: this port is plain HTTP. --trusted-proxies- Required for a proxy on another machine: its own address or subnet.
Without flags, ./setup.sh asks: under “The way in”, choose 2. A running installation first asks “Change any of them?”: answer y.
# a proxy on this server
./setup.sh \
--behind-proxy https://automations.example.com \
--app-port 127.0.0.1:8080# a load balancer on another machine
./setup.sh \
--behind-proxy https://automations.example.com \
--app-port 10.0.1.5:8080 \
--trusted-proxies 10.0.1.0/242.Point your proxy at GoRunner
Whatever the proxy, it must:
- Pass every path
/,/api,/embed,/embed.js,/embed-sdk,/mcp,/oauthand/.well-known, to one place./apiholds webhooks and hosted forms too;/mcp,/oauthand/.well-known/oauth-…connect AI apps.- Name the caller
- Keep
Host, and setX-Forwarded-Forto the connecting address, replacing what the caller sent. - Stream
- No response buffering, and at least 60 seconds of quiet; an hour is safe. GoRunner sends a keep-alive every 25 seconds.
- Take 6 MiB
- GoRunner takes request bodies up to 5 MiB. nginx refuses over 1 MB unless raised.
- Add no security headers
- No
X-Frame-OptionsorContent-Security-Policy.Strict-Transport-Securityis yours to add. - Compress nothing it must hold back
- GoRunner compresses the dashboard’s files itself. Compressing API answers is fine, but never by buffering a stream.
A health check can ask GET /api/ready: 200 when GoRunner reaches its database and Redis. See Health checks.
An AWS Application Load Balancer?
- Target group
- HTTP, the
--app-portport on the server’s private address, health check/api/ready, success code200. - Listener
- HTTPS 443 with your certificate from ACM.
- Idle timeout
- The default 60 seconds works. Never lower it.
- Trusted ranges
- The subnets the load balancer runs in, such as
10.0.1.0/24 10.0.2.0/24.
It streams, takes any body size and adds X-Forwarded-For itself. Open the server’s port to the load balancer only.
upstream gorunner {
server 127.0.0.1:8080;
keepalive 16;
}
server {
listen 443 ssl;
http2 on;
server_name automations.example.com;
ssl_certificate /etc/ssl/automations.example.com.crt;
ssl_certificate_key /etc/ssl/automations.example.com.key;
client_max_body_size 6m;
location / {
proxy_pass http://gorunner;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 1h;
}
}frontend https
bind :443 ssl crt /etc/haproxy/certs/automations.example.com.pem
mode http
http-request set-header X-Forwarded-For %[src]
http-request set-header X-Forwarded-Proto https
default_backend gorunner
backend gorunner
mode http
option httpchk GET /api/ready
http-check expect status 200
timeout server 1h
server app 127.0.0.1:8080 checkhttp:
routers:
gorunner:
rule: Host(`automations.example.com`)
entryPoints: [websecure]
service: gorunner
tls:
certResolver: letsencrypt
services:
gorunner:
loadBalancer:
servers:
- url: http://127.0.0.1:8080
healthCheck:
path: /api/ready
interval: 10sBehind a CDN, let nginx learn the caller first: set_real_ip_from <its range>; and real_ip_header X-Forwarded-For;. Traefik replaces forwarding headers a caller sent, unless the caller is in its entry point’s forwardedHeaders.trustedIPs.
3.Check it
- Open your address. The sign-in page appears, with your proxy’s certificate.
- Run a workflow from the editor. Its steps light up one by one: streaming works.
- Open Platform › Audit log. Your sign-in shows your computer’s address, not the proxy’s.
curl -sS http://127.0.0.1:8080/api/readyThe address ranges to trust
Sign-in limits, an embed key’s address list and the audit log all need the caller’s real address. GoRunner reads X-Forwarded-For only from a connection it trusts, from the right, and takes the first address it doesn’t trust. It then drops Forwarded and True-Client-IP, and overwrites X-Real-IP and X-Forwarded-For with that address.
| Your proxy | Trust |
|---|---|
On this server, --app-port on 127.0.0.1 | Nothing to give. The setup writes gateway, which only programs on this server can use. |
| On another machine | Its own address or subnet. Required. |
| A CDN in front of your proxy | Let your proxy learn the caller from it. Or add its range: on this server, it adds to gateway. |
Give ranges separated by spaces or commas. The setup refuses private_ranges: it would trust every container on the server, workflow steps included. A range covering every address stops GoRunner from starting.
Warning A trusted range is believed about who is calling. Trust one that ordinary callers can reach, and they get past the sign-in limits and an API key’s address list, and into the audit log under another address.
A proxy in front of automatic HTTPS?
Name it with --trusted-proxies; on this server, with Caddy’s ports on 127.0.0.1, that is gateway. Caddy never trusts a range holding one of Docker’s networks, so private_ranges is refused there too. Name an IPv6 proxy by its exact address.
For Embed
Embed needs sign-in on, a license that includes Embedding, and one https address: an https page can’t frame an http installation. Pass /embed/ untouched: no cache, no framing header of your own. Without the license or sign-in, /embed/ answers 404.
/embed/- The embed document, only to your allowed websites. Its
frame-ancestorsnames only the page it was opened for, withCache-Control: no-store. /embed.js- The SDK, always the current version:
no-cachewith anETag. /embed-sdk/<version>/embed.js- One version, never changed, cached for a year. Pin it with Subresource Integrity.
/api/embed/v1/…- The embed API and the frame’s sign-in.
GoRunner decides on every load, from the installation’s and each customer’s allowed websites; they never appear in a header. /embed.html is always 404. The SDK’s files answer Access-Control-Allow-Origin: * and Cross-Origin-Resource-Policy: cross-origin. The rest of the dashboard sends X-Frame-Options: SAMEORIGIN, the AI app consent screen DENY. See Set up Embed.
Set up behind a proxy with v2026.10.0 or v2026.10.1?
It keeps GoRunner’s Caddy between your proxy and the app, on the same port. To connect directly, run ./setup.sh, answer y to “Change any of them?”, and choose 2 under “The way in”. Then point your proxy at the address it gives.
A proxy that asks GET /api/embed/frame-policy itself must add ?parent=<the embed document’s parent> from v2026.10.4. Pointing it at the app as above needs none of that.
Related
- A company proxyFor traffic leaving GoRunner · 5 min
- Health checks/api/ready, and who sees the details
- TroubleshootingThe address doesn’t open, and more
Checked against 12d85b3b