Deployment
Four ways to reach the panel, from a laptop to a public domain — and the one setting that decides whether your rate limiting is real.
The panel binds to 127.0.0.1 and nothing else. That is not a
default waiting to be changed: it grants full RCON access to whoever signs in,
so making it reachable should be a decision you take on purpose. Every recipe
below keeps the container on loopback and puts something in front of it.
TRUST_PROXY=true the
panel ignores X-Forwarded-For — deliberately, because a
forgeable header must not drive rate limiting — so the per-IP limiter is
skipped entirely and only the global cap is left standing
(src/server/http/context.ts). With it set behind a proxy that
does not overwrite the header,
an attacker picks their own identity and the limiter is bypassable. Set it
only behind a proxy you control that rewrites the header
itself. All four recipes below do.
Local, on your own machine#
Nothing in front, nothing to configure. This is the quickstart, and it is also the right answer for a server you administer over SSH — forward the port instead of publishing it.
docker compose up -d
# → http://127.0.0.1:3010
# From another machine, without exposing anything:
ssh -N -L 3010:127.0.0.1:3010 you@your-serverCaddy#
The shortest path to HTTPS: Caddy obtains and renews the certificate on its
own, and sets X-Forwarded-For itself.
factorio.example.com {
reverse_proxy 127.0.0.1:3010
}Then add the variable to the factorio-admin service:
environment:
- TRUST_PROXY=trueNginx#
The proxy_set_header lines are the point: Nginx passes an
inbound X-Forwarded-For through unless you overwrite it, and
$proxy_add_x_forwarded_for appends rather than replaces.
Setting it to $remote_addr discards whatever the client claimed.
server {
listen 443 ssl http2;
server_name factorio.example.com;
ssl_certificate /etc/letsencrypt/live/factorio.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/factorio.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3010;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Traefik#
With Traefik on the same compose network, drop the port publishing entirely
and label the service. Traefik writes X-Forwarded-For itself for
any client that is not in trustedIPs.
factorio-admin:
# No `ports:` at all — Traefik reaches the container over the network.
environment:
- TRUST_PROXY=true
labels:
- "traefik.enable=true"
- "traefik.http.routers.factorio-admin.rule=Host(`factorio.example.com`)"
- "traefik.http.routers.factorio-admin.entrypoints=websecure"
- "traefik.http.routers.factorio-admin.tls.certresolver=letsencrypt"
- "traefik.http.services.factorio-admin.loadbalancer.server.port=3000"Tailscale#
The option with the smallest attack surface: no public port, no certificate to renew, no reverse proxy to keep patched. The panel is reachable from your tailnet and from nowhere else, which suits an admin console better than a public hostname does.
# On the host running the stack
tailscale serve --bg --https=443 http://127.0.0.1:3010
# → https://your-host.your-tailnet.ts.net
tailscale serve statustailscale serve terminates TLS and forwards over loopback, so it
is a reverse proxy like the others: set TRUST_PROXY=true, or
every tailnet client shares one rate-limit bucket.
Do not use tailscale funnel unless you mean to
publish the panel to the whole internet.
Checking it worked#
# Readiness: config, command catalogue, database and RCON, one object each.
curl -s https://factorio.example.com/api/ready | python3 -m json.tool
# The cookie must come back Secure over HTTPS.
curl -si https://factorio.example.com/api/login \
-H 'Content-Type: application/json' -H 'Origin: https://factorio.example.com' \
-d '{"password":"wrong"}' | grep -i set-cookieA failed login should answer 401, and repeated attempts should
start answering 429. If they never do, the panel is not seeing
distinct client addresses — re-read TRUST_PROXY above.