Reverse proxy, Cloudflare and firewall
Put Turmite behind Caddy, nginx or Cloudflare, keep the real client IP, and close every other port.
The api process speaks plain HTTP on port 8080. A reverse proxy in front of it handles TLS. The install script sets up Caddy for this. The worker opens no port.
Caddy (default)
The install script starts Caddy with automatic TLS. Caddy gets a certificate for the host of the site URL and renews it. Ports 80 and 443 must be open to the internet for this. The site address comes from TURMITE_CADDY_ADDRESS in .env. turmite init sets it to the host of the site URL.
The main parts of /opt/turmite/Caddyfile:
{
servers {
trusted_proxies static {$TURMITE_TRUSTED_PROXIES:private_ranges}
}
}
(turmite) {
encode zstd gzip
request_body {
max_size 4MB
}
header ?Strict-Transport-Security "max-age=31536000"
header ?X-Content-Type-Options "nosniff"
header ?Referrer-Policy "same-origin"
header ?Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()"
header ?Cross-Origin-Opener-Policy "same-origin"
header -Server
header -Via
reverse_proxy api:8080 {
header_up -Forwarded
}
}
{$TURMITE_CADDY_ADDRESS} {
import turmite
}
Turmite sets its own content security policy, frame, cache and referrer headers per page. On an https:// site URL it also sends Strict-Transport-Security: max-age=31536000. The ? headers in Caddy apply only when Turmite did not set the header.
After you change the Caddyfile, restart Caddy:
cd /opt/turmite
sudo docker compose restart caddy
install.sh --upgrade keeps your Caddyfile. If a release changes it, the script writes Caddyfile.new next to it.
For a project's checkout domain, see Give a project a checkout domain.
Your own proxy
You can use another proxy, such as nginx. The compose file does not publish the api port. Publish it on the loopback interface, and turn off the bundled Caddy, in /opt/turmite/docker-compose.override.yml:
services:
api:
ports:
- "127.0.0.1:8080:8080"
caddy:
profiles: [disabled]
Docker Compose reads this file automatically. install.sh --upgrade does not change it. On a new install, run the install script with --no-start, write the file, then run sudo docker compose up -d.
Point the proxy at the api, and pass the Host header and the client IP:
server {
listen 443 ssl;
server_name pay.example.com;
# ssl_certificate and ssl_certificate_key lines here
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
client_max_body_size 4m;
}
}
Never publish port 8080 on a public interface.
Rules for every proxy:
- Pass the original
Hostheader. Turmite uses it to find checkout domains. - Do not change the
Originheader. The dashboard refuses aPOSTwhoseOrigindoes not match the site URL. - Allow request bodies of at least 4 MB. A logo upload can be 2 MB, plus the form data.
To serve Turmite under a path of another site, see Run under a path.
Client IP addresses
Turmite uses the client IP for rate limits and the audit log. The limits are 10 sign-in attempts a minute per IP address and per email address, 600 merchant API requests a minute per IP address, and 300 checkout requests a minute per IP address.
Turmite reads X-Forwarded-For only when the connection comes from an address in TURMITE_TRUSTED_PROXIES. It then uses the rightmost address in the header that is not a trusted proxy. The default list is loopback and the private ranges, which include the Docker network that Caddy uses.
The bundled Caddy reads the same variable for its own list of trusted proxies. If a proxy connects from a public address, set the list in .env. The list replaces the default, so add the private ranges again. Separate the ranges with spaces, because Caddy does not accept commas:
TURMITE_TRUSTED_PROXIES=10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 203.0.113.7/32
Then run sudo docker compose up -d in /opt/turmite.
If the setting is wrong, all visitors look like one IP address. Then one person can lock everyone out of sign-in for a minute, and busy checkouts hit the limit.
Cloudflare
Cloudflare's proxy can sit in front of the server. Visitors then see Cloudflare addresses, and you can block paths before they reach the server.
- Add the domain to Cloudflare.
- In DNS → Records, add the
Arecord (andAAAAif you have IPv6) with Proxy status: Proxied. - In SSL/TLS → Overview, set the mode to Full (strict). Do not use Flexible. It sends traffic to your server without encryption and causes redirect loops.
- Trust Cloudflare's addresses. See the next section.
- Restrict ports 80 and 443 to Cloudflare's addresses. See Firewall.
Keep the real visitor IP
Behind Cloudflare, every request comes from a Cloudflare address. Cloudflare adds the visitor's address to X-Forwarded-For. Add Cloudflare's ranges to TURMITE_TRUSTED_PROXIES. Caddy then keeps the header, and Turmite takes the visitor's address from it.
cf=$({ curl -fsS https://www.cloudflare.com/ips-v4; echo; curl -fsS https://www.cloudflare.com/ips-v6; } | xargs)
echo "TURMITE_TRUSTED_PROXIES=10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 $cf" | sudo tee -a /opt/turmite/.env
cd /opt/turmite && sudo docker compose up -d
Keep only one TURMITE_TRUSTED_PROXIES line in .env. Cloudflare changes its ranges rarely. Run the command again from time to time, and remove the old line.
Certificates behind Cloudflare
Caddy still gets its own certificate behind Cloudflare with Full (strict). Cloudflare challenges and "Under Attack" mode can block the certificate check. The certificate then fails to renew, and visitors see error 525 or 526.
A Cloudflare Origin Certificate avoids this. Only Cloudflare trusts it, which is enough behind the proxy.
-
In Cloudflare, open SSL/TLS → Origin Server → Create Certificate.
-
List the host names, for example
pay.example.com. -
Save the certificate as
/opt/turmite/origin.pem. -
Save the private key as
/opt/turmite/origin-key.pem. Cloudflare shows the key one time only. -
Run
sudo chmod 600 /opt/turmite/origin-key.pem. -
Add a
tlsline to the site block of the Caddyfile:{$TURMITE_CADDY_ADDRESS} { import turmite tls /etc/caddy/origin.pem /etc/caddy/origin-key.pem } -
Mount the files into the Caddy container, in
/opt/turmite/docker-compose.override.yml:services: caddy: volumes: - ./origin.pem:/etc/caddy/origin.pem:ro - ./origin-key.pem:/etc/caddy/origin-key.pem:ro -
Run
sudo docker compose up -d caddy.
Each Cloudflare zone needs its own certificate. A checkout domain in a different zone, such as pay.shop-two.com, needs a second certificate and its own site block.
Bot protection and your shop
Cloudflare Bot Fight Mode and a high Security Level can challenge requests that do not come from a browser. Your shop's server then gets 403 from /v1/. To fix this, add a WAF custom rule with the action Skip for paths that start with /v1/. Or turn off Bot Fight Mode for the zone.
Webhooks leave your server directly. Cloudflare does not touch them.
Firewall
Open only these ports:
| Port | From | Use |
|---|---|---|
| 22 | Your IP addresses | SSH |
| 80, 443 | Anywhere, or only Cloudflare's ranges when you use Cloudflare | HTTP and HTTPS |
Never publish PostgreSQL (5432) or the api port (8080). The compose file keeps PostgreSQL on an internal network with no route to the internet.
Docker bypasses ufw
Docker writes its own iptables rules for published ports. These rules come before the ufw rules. A ufw rule does not stop traffic to a port that Docker publishes.
Use one of these:
- The firewall of your hosting provider. It filters traffic before it reaches the server, so Docker cannot change it. This is the better choice.
- The
DOCKER-USERiptables chain. Docker reads this chain before its own rules.
Example: let only Cloudflare reach ports 80 and 443.
for r in $(curl -fsS https://www.cloudflare.com/ips-v4); do
sudo iptables -I DOCKER-USER -i eth0 -s "$r" -p tcp -m multiport --dports 80,443 -j RETURN
done
sudo iptables -A DOCKER-USER -i eth0 -p tcp -m multiport --dports 80,443 -j DROP
sudo apt install iptables-persistent
sudo netfilter-persistent save
Replace eth0 with your public interface. ip route get 1.1.1.1 shows it. Do the same with ip6tables for IPv6. Caddy also listens on UDP 443 for HTTP/3. Add the same rules for -p udp to filter it too.
Check the firewall
From another machine, connect to the server IP directly. Behind Cloudflare, this command must time out:
curl -sk --max-time 10 --resolve pay.example.com:443:198.51.100.10 https://pay.example.com/healthz
Replace 198.51.100.10 with the server's public IP address.
Next
To keep the dashboard off the public internet, see Private dashboard.