Overview

When running large Drupal installations, you may find yourself with a web server cluster that lives behind a load balancer. The pages here contain tips for configuring Drupal in this setup, as well as example configurations for various load balancers.

In addition to a large selection of commercial options, various open source load balancers exist: HAProxy, Pound, Varnish, ffproxy, tinyproxy, etc. Web servers (including Apache and NGINX) can also be configured as reverse proxies.

The basic layout you can expect in most high-availability environments will look something like this:

 
Browser
 
──→
 
HTTP Reverse Proxy
    ┌─→
──┼─→
    └─→
Web server 1
Web server 2
Web server 3

→ Database

By way of explanation:

  • Browsers will connect to a reverse proxy using HTTP or HTTPS. The proxy will in turn connect to web servers via HTTP.
  • Web servers will likely be on private IP addresses. Use of a private network allows web servers to share a database and/or NFS server that need not be exposed to the Internet on a public IP address.
  • If HTTPS is required, it is configured on the proxy, not the web server.

Most HTTP reverse proxies will also "clean" requests in some way. For example, they'll require that a browser include a valid User-Agent string, or that the requested URL contain standard characters or not exceed a certain length.

In the case of Drupal, it is highly recommended that all web servers share identical copies of the Drupal DocumentRoot in use, to ensure version consistency between themes and modules. This may be achieved using an NFS mount to hold your Drupal files, or by using a revision control system (CVS, SVN, git, etc) to maintain your files.

High availability

In order to achieve the maximum uptime, a high-availability design should have no single points of failure. For network connectivity, this may mean using BGP with multiple upstream providers, as well as perhaps using Link Aggregation (LACP) to maintain multiple physical network paths in your LAN. In the diagram above, the two server elements that need attention are the load balancer and the database.

A load balancer cannot easily be "clustered" because a single IP address usually needs to apply to a single machine. To address this issue, you may wish to read up on CARP and Corosync / Pacemaker (formerly Heartbeat).

A database server generally needs access to a single repository of data. Various technologies exist to address this, including MySQL NDB, Galera Cluster (for both MySQL and MariaDB) and PgCluster. If you're willing to accept the possibility of less than 100% up-time while you recover from broken hardware, you should consider using transactional database replication to keep a live copy of your data on a secondary server. Read the documentation for your database server software to find out how to set this up.

Needless to say, always set up regular automated backups.

Configuration

The configuration required to enable this environment consists of:

  • Setting the configuration value reverse_proxy to TRUE,
  • Setting the configuration value reverse_proxy_addresses to an array containing the IP of HTTP Reverse Proxy, as seen from the web servers.

Drupal 8 reverse proxy configuration

The default Drupal 8 settings.php will contain detailed information on the following variables:

  • $settings['reverse_proxy']
  • $settings['reverse_proxy_addresses']
  • $settings['reverse_proxy_trusted_headers']

A more detailed explanation on why these settings are needed can be found in the article Drupal 8 and reverse proxies: The $base_url drama.

If you only need these settings on a production site, you can place these variables in a settings.local.php file. Read the comments and uncomment the section of code at the bottom of your settings.php to set this up.

Example

Here's an example that demonstrates how to tell Drupal sites that they're running behind an HTTPS proxy, which terminates the encryption before getting to the Web server. While the Web server doesn't need to handle HTTPS requests, Drupal sites still need to know that they're being accessed that way.

This code will work for both Drupal 7 and Drupal 8. It's the (by default empty) global.inc file for an Aegir installation, which gets injected into all hosted sites' settings.php files.

The same code can be used for any site, either in settings.php or settings.local.php.

<?php # global settings.php

/**
 * Tell all Drupal sites that we're running behind an HTTPS proxy.
 */

// Drupal 7 configuration.
if (explode('.', VERSION)[0] == 7) {
  $conf['reverse_proxy'] = TRUE;
  $conf['reverse_proxy_addresses'] = ['1.2.3.4', ...];

  // Force the protocol provided by the proxy. This isn't always done
  // automatically in Drupal 7. Otherwise, you'll get mixed content warnings
  // and/or some assets will be blocked by the browser.
  if (php_sapi_name() != 'cli') {
    if (isset($_SERVER['SITE_SUBDIR']) && isset($_SERVER['RAW_HOST'])) {
      // Handle subdirectory mode (e.g. example.com/site1).
      $base_url = $_SERVER['HTTP_X_FORWARDED_PROTO'] . '://' . $_SERVER['RAW_HOST'] . '/' . $_SERVER['SITE_SUBDIR'];
    }   
    else {
      $base_url = $_SERVER['HTTP_X_FORWARDED_PROTO'] . '://' . $_SERVER['SERVER_NAME'];
    }   
  }
}
// Drupal 8 configuration.
else {
  $settings['reverse_proxy'] = TRUE;
  $settings['reverse_proxy_addresses'] = ['1.2.3.4', ...];
  // See https://symfony.com/doc/current/deployment/proxies.html.
  $settings['reverse_proxy_trusted_headers'] = \Symfony\Component\HttpFoundation\Request::HEADER_X_FORWARDED_ALL
}

Notes

  • If you plan to install Drupal 7 on a web server that browsers will reach only via HTTPS, there's an outstanding issue you'll want to check (#2970929: [D7] Support X-Forwarded-* HTTP headers alternates). At this time, Drupal's AJAX callbacks use URLs based on the protocol used at the web server, regardless of the protocol used at the proxy. Your workaround is either this patch, or to set the "reverse_proxy" variable manually in your settings.php file. Unfortunately, as the Drupal installer relies on AJAX, your only other option is to install via HTTP instead of HTTPS. This is no longer a problem in Drupal 8+.
  • When setting the reverse proxy addresses, do not use $_SERVER['REMOTE_ADDR'], the client IP address sent in the request, as it can be spoofed (unless you have a process in place to mitigate this).

Comments

jmsosso’s picture

I would add $_SERVER['HTTPS'] = 'on' in order to get drupal_is_https() returns the right value.

if (php_sapi_name() != 'cli' && isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
  if (isset($_SERVER['SITE_SUBDIR']) && isset($_SERVER['RAW_HOST'])) {
    // Handle subdirectory mode (e.g. example.com/site1).
    $base_url = $_SERVER['HTTP_X_FORWARDED_PROTO'] . '://' . $_SERVER['RAW_HOST'] . '/' . $_SERVER['SITE_SUBDIR'];
  }
  else {
    $base_url = $_SERVER['HTTP_X_FORWARDED_PROTO'] . '://' . $_SERVER['SERVER_NAME'];
  }
  if ($_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') {
    $_SERVER['HTTPS'] = 'on';
  }
}

Just for Drupal 7.

elkin_taharon’s picture

Is there a way to use a ip range instead of the whole list of IPs addresses ?

Currently i'm using projectshield by google, theirs ip range is 35.235.224.0 / 20 (More the 4000 IPs if i'm not wrong) and i wondered if there is posible to use it directly.

Thanks in advance,

Elkin.

colan’s picture

Not yet, but there's an open issue for that, which you can help with: #2466417: Add network range support for reverse_proxy_address in ip_address().

jukka792’s picture

Hi,
I do not understand, I have been running D7 and D8 and now D9 for multiple years so that I have a Haproxy load balancer and behind that 1-3 application servers. All of the server have multiple D8 and D7 running in multisite setup.
I have never set anything in the settings.php (reverse proxy) like mentioned in this post. It still works.

Only thing what I have done is the NFS file server where are all the /sites/multisitename/files <- files folders. So only the "files" folder is mounted from NFS.

bburg’s picture

@jukka792, Drupal will generally work without the reverse proxy settings. What happens if you don't use them though are some odd issues. For example:

  • Webforms, and other statistics, will log your proxy IP address as the one that submitted the form.
  • The flood table will block your proxy IP if one of your admins has too many failed login attempts. Which will effectively lock everyone out of the site. the only fix being to wait until the flood table expiration, or a dev goes in and truncates the flood table.

Basically, everything on the site will see requests coming from that one IP address, and not differentiate users.

Metatag should be in core.

bburg’s picture

So I have a setup that uses Fastly → Haproxy → Drupal, and I had been using the snippet I define here: https://www.drupal.org/project/fastly/issues/2832820#comment-13137591. I'm not sure if something changed on Fastly's end or what, but now I seem to receive requests, without a chained value for $_SERVER['HTTP_X_FORWARDED_FOR'] like I previously expected (e.g. "a.b.c.d, e.f.g.h"). Now it's just a single value that appears to be the IP for Fastly.

I'm not entirely sure what I set for this? I used to have: $settings['reverse_proxy_addresses'] = array_merge($haproxy_ip, $fastly_ip);

and that worked fine, though it looks like this will be deprecated in 9.

To summarize, there are the $_SERVER values:

$_SERVER['HTTP_FASTLY_CLIENT_IP'] (My personal IP)

$_SERVER['HTTP_X_FORWARDED_FOR'] (Fastly)

$_SERVER['REMOTE_ADDR'] (Haproxy)

All of these seems to include a single IP address, and I don't really know how to tell Drupal to use HTTP_FASTLY_CLIENT_IP if that's not in the pre-defined set of constants in \Symfony\Component\HttpFoundation\Request ?

It doesn't feel right, but this Symfony documentation appears to be saying that we should just overwrite the server constants: https://symfony.com/doc/current/deployment/proxies.html

Edit 3/12/2021: I finally got around to test editing the server constants in settings.php. It doesn't work for the purpose of setting the remote address with whatever value is of HTTP_FASTLY_CLIENT_IP. That is because it seems that Symfony has setup HttpFoundation->ServerBag already at this point, which populates its $properties with those server vars. So overwriting them in settings.php is ineffective, because those aren't the values that are used.

The only way it seems that you can overwrite these properties is by using \Drupal::request()->server->set(). However, at the point the settings.php file is loaded, the Drupal container doesn't seem to be initialized (which is a bit paradoxical to me, since HttpFoundation->ServerBag appears to capture these properties before settings.php is loaded), and this code will throw an "Drupal\Core\DependencyInjection\ContainerNotInitializedException."

Now, I may be missing some way to use the reverse proxy settings, but it appears that you will need to override the ServerBag properties in an EventSubscriber during KernelEvents::REQUEST. Here is my working example, which seems to allow me to set the value used to save webform submissions.

namespace Drupal\my_module\EventSubscriber;

use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class MyModuleSubscriber implements EventSubscriberInterface {

  public  function setRemoteAddress(GetResponseEvent $event) {
    if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])
      && isset($_SERVER['HTTP_FASTLY_CLIENT_IP'])) {
      \Drupal::request()->server->set('REMOTE_ADDR', $_SERVER['HTTP_FASTLY_CLIENT_IP']);
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events[KernelEvents::REQUEST][] = array('setRemoteAddress');
    return $events;
  }
}

This of course needs a corresponding entry in a my_module.services.yml file.

Metatag should be in core.

crutch’s picture

This is all good and everything, and we have been using this configuration on D7 for 6-7 years. When https became required we ran into this issue https://www.drupal.org/project/drupal/issues/2970929

But, there is no good way to handle files or log in persistence unless there is a Parent server with one way replication to child servers, and you ensure that logged in users are always working on Parent. If we didn't do this then they would be load balanced to another server and not be logged in. We did this by forcing logged in users to Parent server by address like

/admin/*
/admin_menu/*
/node/add
/node/add/*
etc.

A complete list of admin addresses would be great to have for this reason. I think we have missed some but generally all is working okay.

For files, we could offload to a separate file server but the log in persistence issue would still exist.

pramod.kmr73’s picture

Drupal 8.8.7 , when accessed directly from a web browser /postman the site works perfectly okay with the http basic authentication enabled .
When the same is accessed thru a Load Balancer(F5) configured as reverse proxy , the access fails with authentication error.
Is some configuration required. This was working without any settings till few days back and the request started to fail though there
is no change in any configuration.

rossidrup’s picture

I am having problem understanding this
So I need to add these three lines in settings.php? Or better yet uncomment them? The last line is not in my settings.php
I am on d9

$settings['reverse_proxy']
$settings['reverse_proxy_addresses']
$settings['reverse_proxy_trusted_headers']

Should I input my IP inside? or just paste those 3 lines like they are?

bburg’s picture

You will want to use values from your environment, and references to the headers that are sent in the request. If you want to see what these values are, you can look at the phpinfo by clicking on "more information" next to the PHP version number in your site status report in the environment where you have a proxy set up. Here is an example from a site I have on Pantheon... with a Cloudflare CDN:

  $settings['reverse_proxy'] = TRUE;
  $settings['reverse_proxy_addresses'] = [$_SERVER['REMOTE_ADDR']];
  $settings['reverse_proxy_header'] = 'HTTP_TRUE_CLIENT_IP';
  $settings['reverse_proxy_proto_header'] = 'HTTP_X_FORWARDED_PROTO';
  $settings['reverse_proxy_host_header'] = 'HTTP_X_FORWARDED_HOST';

The thing here that is likely to change it "HTTP_TRUE_CLIENT_IP" which is a Cloudflare specific value. To determine what you need to use, figure out your IP address, and then look at the phpinfo report, wherever you see your actual IP address, use that corresponding value for $settings['reverse_proxy_header'].

I'm not entirely certain if using the $_SERVER['REMOTE_ADDR'] is correct, but it does seem to work if you know your setup.

Edit, some other notes:
$_SERVER['HTTP_X_FORWARDED_FOR']

May contain a list of IPs, which is the series of clients between the server and the user, including the user's at the beginning, e.g. X.X.X.X, X.X.X.X, Which is what I was attempting to parse in my comment earlier.

Metatag should be in core.

rossidrup’s picture

i am not an expert in these server elements.
I want to say that I have 3 drupal 9 sites on my hosting, same account, same htaccess, same theme and are on same cloudflare account...two of them work fine, and one is not....it gives mixed content error
why would other two sites work without problem?
All I did is use https redirect in cloudflare....that is all that is enabled...
by ip you mean ip of my host? Or cloudflare hosting?

it is hard to understand from you post
for example
'HTTP_TRUE_CLIENT_IP';
should I replace this with shared hosting IP?

for example if shared hosting ip is 5.435.435.546 ror example can you show me how to use yourcode insettings.php? I really want to learn this...

zuhair_ak’s picture

For my usecase, we had Imperva CDN, so had to check their docs for their IP ranges and add it as below

$settings['reverse_proxy'] = TRUE;
// https://docs.imperva.com/howto/c85245b7 <- Imperva CDN IP range, update if doc updated
$settings['reverse_proxy_addresses'] = ['199.83.128.0/21', '198.143.32.0/19', '149.126.72.0/21', '103.28.248.0/22', '45.64.64.0/22', '185.11.124.0/22', '192.230.64.0/18', '107.154.0.0/16', '45.60.0.0/16', '45.223.0.0/16', '131.125.128.0/17', '2a02:e980::/29'];
$settings['reverse_proxy_trusted_headers'] = \Symfony\Component\HttpFoundation\Request::HEADER_X_FORWARDED_ALL | \Symfony\Component\HttpFoundation\Request::HEADER_FORWARDED;