Synthigy serves plain HTTP on one port (SYNTHIGY_SERVER_PORT, default 7887).
It terminates no TLS and does no virtual hosting, so an internet-facing
instance sits behind nginx, Caddy, Traefik or a cloud load balancer. This page
is what that proxy has to do; copy-paste configs are at the bottom.
Session and login cookies are Secure, so a browser drops them over plain
http:// on anything but localhost: the login page accepts the password and
comes straight back to itself. Terminate TLS at the proxy and keep the engine
on loopback:
SYNTHIGY_SERVER_HOST=127.0.0.1
SYNTHIGY_SERVER_PORT=7887
SYNTHIGY_IAM_ROOT_URL=https://synthigy.example.com
SYNTHIGY_SERVER_TRUST_PROXY=true
SYNTHIGY_IAM_ROOT_URL is the public origin: the token issuer and the
base of every login and redirect link. Unset, they are derived per request
from X-Forwarded-Host and X-Forwarded-Proto — fine on a laptop, wrong
behind a load balancer, and steerable by whoever can set those headers. The
portal flags it until it is set.
SYNTHIGY_SERVER_TRUST_PROXY records that a proxy is in front and the
client address comes from X-Forwarded-For. The released engine reads that
header whether or not the flag is set; set it anyway, so the deployment says
what it relies on.
Synthigy takes the client address from the first value of
X-Forwarded-For whenever the header is present, and nothing checks who wrote
it. An in-flight login is pinned to that address, so a stolen login state
replayed from elsewhere is rejected — a check worth something only when the
caller cannot choose the address. Two rules follow.
Never expose the engine port directly once a proxy is in front. Bind it to loopback, or firewall it; a request that reaches 7887 around the proxy carries its own idea of who sent it.
Have the proxy overwrite the header, not append to it. The idiomatic nginx
value $proxy_add_x_forwarded_for appends to whatever the client sent, which
leaves the client's own value in first position — exactly the slot that is
read:
| proxy | directive |
|---|---|
| nginx | proxy_set_header X-Forwarded-For $remote_addr; |
| Caddy | header_up X-Forwarded-For {remote_host} |
| Traefik | default — leave forwardedHeaders.trustedIPs unset |
Forward X-Forwarded-Proto and X-Forwarded-Host too; the OAuth layer builds
absolute URLs from them whenever the root URL is unset. If a second proxy or a
cloud load balancer sits in front, only the outermost hop can establish the
client address: have that hop write the header and the inner one pass it
through unchanged.
| path | shape |
|---|---|
/data/events | SSE, keepalive every 20s, open until SYNTHIGY_SERVER_SSE_MAX_LIFETIME_MS (15 minutes by default) |
/console/live/* | SSE, heartbeat every 15s, recycled every 90s |
Both need response buffering and compression off on those paths and a read timeout well above the keepalive interval. A buffering proxy holds events until its buffer fills — which looks like a frozen console and a subscription that never fires, not like a proxy problem. There is no WebSocket endpoint, so no upgrade handling is needed.
/data, /oauth/*, /console/*, /.well-known/*). Pass the URI through
unrewritten — no StripPrefix, no mounting under a subpath.Access-Control-*
and Vary: Origin; a second copy makes browsers reject the response
(CORS.md)./data writes, model deploys and /iam/import
arrive as a single POST body. 64m is a workable ceiling; nginx's 1m default
is not.ssh -L 7888:127.0.0.1:7888 <host> instead. The console at
/console on 7887 is a normal route and is proxied like everything else.proxy_pass http://synthigy; names the upstream block; http:// is the
protocol to the engine, which terminates no TLS. Write it with no path after
the host: a bare proxy_pass http://synthigy; forwards the request URI
untouched, while proxy_pass http://synthigy/; makes nginx strip the matched
location prefix, so /console/live/system arrives as /live/system and
404s.
upstream synthigy {
server 127.0.0.1:7887;
keepalive 32;
}
server {
listen 80;
listen [::]:80;
server_name synthigy.example.com;
return 301 https://$host$request_uri;
}
server {
# nginx 1.25.1+ prefers `listen 443 ssl;` plus a separate `http2 on;`, and
# warns that this form is deprecated. This form is the one that also works
# on 1.14–1.24, where `http2 on;` is an unknown directive and nginx refuses
# to start.
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name synthigy.example.com;
ssl_certificate /etc/letsencrypt/live/synthigy.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/synthigy.example.com/privkey.pem;
client_max_body_size 64m;
proxy_http_version 1.1;
# Required by `keepalive` in the upstream block: without it the client's
# own Connection header is forwarded and the pool is never reused.
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# $remote_addr, NOT $proxy_add_x_forwarded_for — see above.
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
location / {
proxy_pass http://synthigy;
proxy_read_timeout 75s;
}
location /console/assets/ {
proxy_pass http://synthigy;
proxy_cache_valid 200 1h;
}
location = /data/events {
proxy_pass http://synthigy;
proxy_buffering off;
proxy_cache off;
gzip off;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
}
location ^~ /console/live/ {
proxy_pass http://synthigy;
proxy_buffering off;
proxy_cache off;
gzip off;
proxy_read_timeout 5m;
proxy_send_timeout 5m;
}
}
synthigy.example.com {
@sse path /data/events /console/live/*
@rest not path /data/events /console/live/*
encode @rest zstd gzip
reverse_proxy @sse 127.0.0.1:7887 {
header_up X-Forwarded-For {remote_host}
flush_interval -1
transport http {
read_timeout 0
compression off
}
}
reverse_proxy 127.0.0.1:7887 {
header_up X-Forwarded-For {remote_host}
}
}
Caddy obtains the certificate itself. {remote_host} replaces the
client-supplied X-Forwarded-For rather than appending to it.
Static (traefik.yml) — with no trustedIPs, Traefik drops a client-supplied
X-Forwarded-For and writes the real peer address itself, which is what
Synthigy needs. Add trustedIPs only for a load balancer in front of Traefik.
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
forwardedHeaders:
insecure: false
certificatesResolvers:
le:
acme:
email: ops@example.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
providers:
file:
filename: /etc/traefik/dynamic.yml
watch: true
Dynamic (dynamic.yml) — one router for the whole host, no path rewriting:
http:
routers:
synthigy:
rule: "Host(`synthigy.example.com`)"
entryPoints:
- websecure
service: synthigy
tls:
certResolver: le
services:
synthigy:
loadBalancer:
servers:
- url: "http://127.0.0.1:7887"
passHostHeader: true
responseForwarding:
flushInterval: "100ms"
As Docker labels instead of the file provider:
labels:
- "traefik.enable=true"
- "traefik.http.routers.synthigy.rule=Host(`synthigy.example.com`)"
- "traefik.http.routers.synthigy.entrypoints=websecure"
- "traefik.http.routers.synthigy.tls.certresolver=le"
- "traefik.http.services.synthigy.loadbalancer.server.port=7887"
- "traefik.http.services.synthigy.loadbalancer.responseforwarding.flushinterval=100ms"
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |