1. The problem I’m having:
I’m running a WordPress site in Docker behind the Cloudflare CDN. After switching from nginx to Caddy with the following config:
{
debug
servers {
# https://caddyserver.com/docs/caddyfile/options#trusted-proxies
# https://www.cloudflare.com/ips/
trusted_proxies static 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32
trusted_proxies_strict
client_ip_headers CF-Connecting-IP CF-Connecting-IPv6 X-Forwarded-For
}
}
In the server log, while client_ip
is correctly set to the client request IP, the remote_ip
still shows the Cloudflare IP:
{
"request": {
"remote_ip": "172.70.49.153", // Cloudflare IP
"client_ip": "45.67.89.10", // Expected client IP
"headers": {
"X-Forwarded-For": ["116.87.139.27"], // Expected client IP
"Cf-Connecting-Ip": ["116.87.139.27"] // Expected client IP
}
// ...other properties
}
}
2. Error messages and/or full log output:
{
"level": "debug",
"ts": 1732891183.3585944,
"logger": "http.reverse_proxy.transport.fastcgi",
"msg": "roundtrip",
"request": {
"remote_ip": "172.70.49.153",
"remote_port": "27700",
"client_ip": "116.87.139.27",
"proto": "HTTP/2.0",
"method": "GET",
"host": "sparanoid.blog",
"uri": "/index.php?ver=2.2.0",
"headers": {
"Cf-Ipcountry": ["SG"],
"Sec-Ch-Ua-Mobile": ["?0"],
"Referer": ["https://sparanoid.blog/wp-admin/profile.php?updated=1"],
"User-Agent": [
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
],
"Sec-Ch-Ua": [
"\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\""
],
"Accept": ["text/css,*/*;q=0.1"],
"Sec-Gpc": ["1"],
"Sec-Ch-Ua-Platform": ["\"macOS\""],
"Accept-Encoding": ["gzip, br"],
"Cdn-Loop": ["cloudflare; loops=1"],
"Dnt": ["1"],
"Sec-Fetch-Mode": ["no-cors"],
"Sec-Fetch-Dest": ["style"],
"X-Forwarded-Host": ["sparanoid.blog"],
"Accept-Language": ["en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7"],
"X-Forwarded-Proto": ["https"],
"Priority": ["u=0"],
"Sec-Fetch-Site": ["same-origin"],
"Cf-Visitor": ["{\"scheme\":\"https\"}"],
"Cookie": ["REDACTED"],
"Cf-Ray": ["8ea35947ab68ae87-NRT"],
"Cf-Connecting-Ip": ["116.87.139.27"],
"X-Forwarded-For": ["116.87.139.27"]
},
"tls": {
"resumed": false,
"version": 772,
"cipher_suite": 4865,
"proto": "h2",
"server_name": "sparanoid.blog"
}
},
"env": {
"SERVER_NAME": "sparanoid.blog",
"SERVER_PROTOCOL": "HTTP/2.0",
"SCRIPT_NAME": "/index.php",
"HTTP_USER_AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"HTTP_SEC_CH_UA": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
"GATEWAY_INTERFACE": "CGI/1.1",
"PATH_INFO": "",
"REQUEST_METHOD": "GET",
"HTTP_X_FORWARDED_PROTO": "https",
"SSL_PROTOCOL": "TLSv1.3",
"HTTP_SEC_CH_UA_MOBILE": "?0",
"HTTP_ACCEPT": "text/css,*/*;q=0.1",
"HTTP_DNT": "1",
"HTTP_SEC_FETCH_MODE": "no-cors",
"QUERY_STRING": "ver=2.2.0",
"REMOTE_PORT": "27700",
"REQUEST_SCHEME": "https",
"HTTP_ACCEPT_LANGUAGE": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
"HTTP_CF_CONNECTING_IP": "116.87.139.27",
"SERVER_PORT": "443",
"SSL_CIPHER": "TLS_AES_128_GCM_SHA256",
"HTTP_CF_IPCOUNTRY": "SG",
"HTTP_REFERER": "https://sparanoid.blog/wp-admin/profile.php?updated=1",
"HTTP_ACCEPT_ENCODING": "gzip, br",
"REMOTE_HOST": "172.70.49.153",
"DOCUMENT_ROOT": "/app/public_html",
"REQUEST_URI": "/wp-content/plugins/search-exclude/build/gutenberg/css/style.css?ver=2.2.0",
"HTTP_CDN_LOOP": "cloudflare; loops=1",
"SERVER_SOFTWARE": "Caddy/v2.8.4",
"HTTP_SEC_GPC": "1",
"HTTP_HOST": "sparanoid.blog",
"SCRIPT_FILENAME": "/app/public_html/index.php",
"HTTP_PRIORITY": "u=0",
"HTTP_SEC_FETCH_SITE": "same-origin",
"AUTH_TYPE": "",
"CONTENT_LENGTH": "",
"CONTENT_TYPE": "",
"REMOTE_ADDR": "172.70.49.153",
"HTTP_X_FORWARDED_HOST": "sparanoid.blog",
"HTTP_X_FORWARDED_FOR": "116.87.139.27",
"HTTP_SEC_CH_UA_PLATFORM": "\"macOS\"",
"HTTPS": "on",
"HTTP_SEC_FETCH_DEST": "style",
"HTTP_CF_VISITOR": "{\"scheme\":\"https\"}",
"HTTP_COOKIE": "",
"HTTP_CF_RAY": "8ea35947ab68ae87-NRT",
"REMOTE_IDENT": "",
"REMOTE_USER": "",
"DOCUMENT_URI": "/index.php"
},
"dial": "php:9000",
"env": {
"DOCUMENT_ROOT": "/app/public_html",
"REQUEST_URI": "/wp-content/plugins/search-exclude/build/gutenberg/css/style.css?ver=2.2.0",
"SERVER_PORT": "443",
"SSL_CIPHER": "TLS_AES_128_GCM_SHA256",
"HTTP_CF_IPCOUNTRY": "SG",
"HTTP_REFERER": "https://sparanoid.blog/wp-admin/profile.php?updated=1",
"HTTP_ACCEPT_ENCODING": "gzip, br",
"REMOTE_HOST": "172.70.49.153",
"HTTP_CDN_LOOP": "cloudflare; loops=1",
"HTTP_SEC_GPC": "1",
"SERVER_SOFTWARE": "Caddy/v2.8.4",
"CONTENT_LENGTH": "",
"CONTENT_TYPE": "",
"HTTP_HOST": "sparanoid.blog",
"SCRIPT_FILENAME": "/app/public_html/index.php",
"HTTP_PRIORITY": "u=0",
"HTTP_SEC_FETCH_SITE": "same-origin",
"AUTH_TYPE": "",
"HTTP_X_FORWARDED_HOST": "sparanoid.blog",
"HTTP_X_FORWARDED_FOR": "116.87.139.27",
"REMOTE_ADDR": "172.70.49.153",
"HTTP_SEC_CH_UA_PLATFORM": "\"macOS\"",
"REMOTE_USER": "",
"DOCUMENT_URI": "/index.php",
"HTTPS": "on",
"HTTP_SEC_FETCH_DEST": "style",
"HTTP_CF_VISITOR": "{\"scheme\":\"https\"}",
"HTTP_COOKIE": "",
"HTTP_CF_RAY": "8ea35947ab68ae87-NRT",
"REMOTE_IDENT": "",
"PATH_INFO": "",
"REQUEST_METHOD": "GET",
"SERVER_NAME": "sparanoid.blog",
"SERVER_PROTOCOL": "HTTP/2.0",
"SCRIPT_NAME": "/index.php",
"HTTP_USER_AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"HTTP_SEC_CH_UA": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
"GATEWAY_INTERFACE": "CGI/1.1",
"HTTP_X_FORWARDED_PROTO": "https",
"REMOTE_PORT": "27700",
"REQUEST_SCHEME": "https",
"SSL_PROTOCOL": "TLSv1.3",
"HTTP_SEC_CH_UA_MOBILE": "?0",
"HTTP_ACCEPT": "text/css,*/*;q=0.1",
"HTTP_DNT": "1",
"HTTP_SEC_FETCH_MODE": "no-cors",
"QUERY_STRING": "ver=2.2.0",
"HTTP_CF_CONNECTING_IP": "116.87.139.27",
"HTTP_ACCEPT_LANGUAGE": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7"
},
"request": {
"remote_ip": "172.70.49.153",
"remote_port": "27700",
"client_ip": "116.87.139.27",
"proto": "HTTP/2.0",
"method": "GET",
"host": "sparanoid.blog",
"uri": "/index.php?ver=2.2.0",
"headers": {
"User-Agent": [
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
],
"Sec-Ch-Ua": [
"\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\""
],
"Cf-Ipcountry": ["SG"],
"Sec-Ch-Ua-Mobile": ["?0"],
"Referer": ["https://sparanoid.blog/wp-admin/profile.php?updated=1"],
"Accept-Encoding": ["gzip, br"],
"Cdn-Loop": ["cloudflare; loops=1"],
"Accept": ["text/css,*/*;q=0.1"],
"Sec-Gpc": ["1"],
"Sec-Ch-Ua-Platform": ["\"macOS\""],
"Priority": ["u=0"],
"Sec-Fetch-Site": ["same-origin"],
"Dnt": ["1"],
"Sec-Fetch-Mode": ["no-cors"],
"Sec-Fetch-Dest": ["style"],
"X-Forwarded-Host": ["sparanoid.blog"],
"Accept-Language": ["en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7"],
"X-Forwarded-Proto": ["https"],
"Cf-Visitor": ["{\"scheme\":\"https\"}"],
"Cookie": ["REDACTED"],
"Cf-Connecting-Ip": ["116.87.139.27"],
"X-Forwarded-For": ["116.87.139.27"],
"Cf-Ray": ["8ea35947ab68ae87-NRT"]
},
"tls": {
"resumed": false,
"version": 772,
"cipher_suite": 4865,
"proto": "h2",
"server_name": "sparanoid.blog"
}
}
}
3. Caddy version:
Caddy/v2.8.4
4. How I installed and ran Caddy:
Caddy is running in Docker Compose with userland-proxy
disabled.
a. System environment:
- Docker version 26.1.4, build 5650f9b
b. Custom build Caddy image:
# https://hub.docker.com/_/caddy
FROM caddy:2.8.4-builder AS builder
RUN xcaddy build \
--with github.com/caddyserver/cache-handler \
--with github.com/caddy-dns/route53 \
--with github.com/caddy-dns/cloudflare \
--with github.com/mholt/caddy-ratelimit \
--with github.com/fvbommel/caddy-dns-ip-range \
--with github.com/WeidiDeng/caddy-cloudflare-ip \
--with github.com/xcaddyplugins/caddy-trusted-cloudfront \
--with github.com/xcaddyplugins/caddy-trusted-gcp-cloudcdn
FROM caddy:2.8.4
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
c. Service/unit/compose file:
caddy:
image: caddy-custom:latest
restart: always
cap_add:
- NET_ADMIN
env_file: ./caddy.env
build:
context: .
dockerfile: Dockerfile-caddy
ports:
- 80:80
- 443:443
- 443:443/udp
volumes:
- ./config/caddy:/etc/caddy
- ./data/caddy_data:/data
- ./data/caddy_config:/config
- /srv/www:/app
d. My complete Caddy config:
{
debug
servers {
# https://caddyserver.com/docs/caddyfile/options#trusted-proxies
# https://www.cloudflare.com/ips/
trusted_proxies static 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32
trusted_proxies_strict
client_ip_headers CF-Connecting-IP CF-Connecting-IPv6 X-Forwarded-For
}
}
sparanoid.blog {
tls {
dns cloudflare {env.CF_API_TOKEN}
}
header {
-Server
}
encode zstd gzip
root * /app/public_html
# debug only
log {
output stdout
format console
}
php_fastcgi php:9000 {
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
}
file_server
}
5. Others:
It partially works since client_ip
, X-Forwarded-For
, and Cf-Connecting-Ip
are correctly set to the user request IP. However, WordPress and most other applications read the remote IP as the true client IP.
For example, in the WordPress dashboard with plugins like Simple History, it gets IP from $_SERVER["REMOTE_ADDR"]
:
_server_remote_addr
: 172.70.49.x_server_http_x_forwarded_for_0
: 116.87.139.x
With nginx using this configuration, $_SERVER["REMOTE_ADDR"]
correctly points to the client IP:
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
...other CF IPs from https://www.cloudflare.com/ips/
real_ip_header CF-Connecting-IP;
Is my Caddy configuration missing something, or is this the expected behavior where remote_ip remains unchanged? Please let me know.