Fixing issue where all user IPs in Discourse deployed via 1Panel show as Cloudflare instead of real browser IPs

I installed 1Panel on my VPS, deployed Discourse (containerized), and set up reverse proxying using OpenResty (containerized). My domain is hosted on Cloudflare with the CDN (yellow cloud) enabled. Below is a tutorial I wrote to solve the issue where users’ IP addresses appear as coming from Cloudflare instead of their actual browser IPs.

Step 1: Create a Scheduled Task in 1Panel to Download the Latest Cloudflare IP List Weekly and Save It to the OpenResty Configuration Directory

  1. In the left sidebar of 1Panel, click “Scheduled Tasks”.

  2. Click “Create Scheduled Task” in the top-right corner.

  3. Fill in the parameters as follows (you can copy and paste directly):

  • Task Type: Select Shell Script.

  • Task Name: For example, Auto-update Cloudflare IP Ranges.

  • Execution Cycle: Recommended to set to Weekly (e.g., every Saturday at 03:00 AM).

  • Script Content: Copy and paste the following complete code block (Note: First, modify the container name and configuration directory path at the top of the script):

#!/bin/bash
# Configuration section
CONTAINER_NAME="Name of the OpenResty container in 1Panel"
# Modify to the specified website proxy directory
CONF_DIR="/opt/1panel/www/sites/www.yourdomain.com/proxy"

echo "[$(date)] Starting to fetch the latest IP ranges from Cloudflare..."
TEMP_DIR=$(mktemp -d)

# Fetch and convert the IP list into Nginx-recognizable format
curl -fsS https://www.cloudflare.com/ips-v4 | sed 's/.*/set_real_ip_from \&;/' > $TEMP_DIR/cf-ips-v4.conf
curl -fsS https://www.cloudflare.com/ips-v6 | sed 's/.*/set_real_ip_from \&;/' > $TEMP_DIR/cf-ips-v6.conf

# Check and move files
if [[ -s $TEMP_DIR/cf-ips-v4.conf ]] && [[ -s $TEMP_DIR/cf-ips-v6.conf ]]; then
    mv $TEMP_DIR/cf-ips-v4.conf $CONF_DIR/
    mv $TEMP_DIR/cf-ips-v6.conf $CONF_DIR/
    echo "[$(date)] Configuration files successfully updated to $CONF_DIR."
else
    echo "[$(date)] Error: Failed to fetch Cloudflare IPs!"
    rm -rf $TEMP_DIR
    exit 1
fi
rm -rf $TEMP_DIR

# Test and reload Nginx
echo "[$(date)] Testing Nginx configuration..."
if docker exec $CONTAINER_NAME nginx -t; then
    docker exec $CONTAINER_NAME nginx -s reload
    echo "[$(date)] Success! OpenResty configuration reloaded."
else
    echo "[$(date)] Failed! Nginx configuration test did not pass."
    exit 1
fi

Once the task is created, you don’t need to wait until Saturday. You can test it immediately: find the task you just created in the “Scheduled Tasks” list.

Click the “Report” button on the right. Check the log window that pops up. If the last few lines display Success! OpenResty configuration reloaded, it means the entire process has been successfully executed within 1Panel!

Step 2: Add realip Configuration to OpenResty

In the host directory /opt/1panel/www/sites/www.yourdomain.com/proxy/, create a new file (the filename can be anything, as long as it ends with .conf; it will be automatically loaded by the main configuration’s include *.conf): realip.conf

real_ip_header CF-Connecting-IP;
real_ip_recursive on;

Step 3: Reload OpenResty

docker exec Name_of_the_OpenResty_container_in_1Panel nginx -t
docker exec Name_of_the_OpenResty_container_in_1Panel nginx -s reload

Step 4: Verify Whether OpenResty Has Retrieved the Real IP

Check the OpenResty access log:

tail -f /opt/1panel/www/sites/www.yourdomain.com/log/access.log

If the configuration is correct, the first IP in the logs should change to your actual public IP, rather than Cloudflare’s 173.245.x.x or similar ranges.

Step 5: Notes Regarding Discourse

Adding - "templates/cloudflare.template.yml" in Discourse’s app.yml is only effective when Discourse directly faces Cloudflare. Since OpenResty reverse proxy is placed in between, the source IP seen by the Discourse container is actually the Docker gateway (e.g., 172.17.0.1 or 172.18.0.1), so the set_real_ip_from <CloudflareIP> in that template will not match.

However, as long as you restore the real IP at the OpenResty layer following the steps above and pass a clean X-Forwarded-For, the Rails backend of Discourse will correctly identify the real user IP, because Rails already trusts Docker private network ranges like 172.x.x.x by default.

Therefore, there is no need to add - "templates/cloudflare.template.yml" in Discourse’s app.yml.