2022-01-26

Fixing the Host header when running a reverse proxy on Google Cloud Run

I’m running this WordPress blog on Google Cloud Run. It’s running on Apache since the official WordPress Docker image doesn’t support Nginx. There is a WordPress Docker image that uses PHP FPM, but you will need to connect to it from another container using FastCGI. This poses some problems in Google Cloud Run since containers can only communicate over HTTP.

Rather than creating my own Docker image where both PHP and Nginx run in the same container, I opted to simply use the Apache-based Docker image. But as everyone knows, Apache doesn’t scale to more than a few concurrent requests, especially when using mod_php. This is because the PHP process is attached to every request, even if it’s a static file. So I decided to use an Nginx reverse proxy in front of the Apache container to serve static assets more effectively and cache dynamic documents using a micro caching strategy.

Here’s a diagram of the entire setup. It’s been slightly simplified and it doesn’t show Google’s frontend server that does load balancing and SSL termination for both of the Cloud Run instances.

But Google’s Cloud architecture presented some unexpected issues. Every Google Cloud Run container is assigned an URL like https://my-apache-8cqci21j0m-lz.a.run.app. This URL is the only way to access the app. This is in contrast to the usual case where Apache is running on a different port on the same server or on a local network on a different hostname. So, in your Nginx configuration, you will have to write something like this:

server { listen 80; listen [::]:80; server_name localhost; location / { proxy_ssl_server_name on; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_set_header Host my-apache-8cqci21j0m-lz.a.run.app; proxy_pass https://my-apache-8cqci21j0m-lz.a.run.app:443; } }

This is assuming your Apache container is running in the URL https://my-apache-8cqci21j0m-lz.a.run.app and the Nginx container in the URL https://example.com.

While this mostly does work, the problem comes with the fact that Google Cloud Run requires the Host header in order to resolve the IP address to the right application. This means that Apache now thinks it’s running in the hostname of my-apache-8cqci21j0m-lz.a.run.app instead of example.com. This causes some issues with applications like WordPress that rely on the Host header to construct their URLs properly.

So, how can we fix this? The answer lies in the little-known header X-Forwarded-Host. This header works like X-Forwarded-For, which is used to pass along the client’s IP address to the backend server, and X-Forwarded-Proto and X-Forwarded-Port which do the same for the protocol and the port. X-Forwarded-Host can be used to pass the original Host header from the client to the backend server.

So, let’s add the X-Forwarded-Host header to our Nginx config:

server { listen 80; listen [::]:80; server_name localhost; location / { proxy_ssl_server_name on; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Host $host; proxy_set_header Host my-apache-8cqci21j0m-lz.a.run.app; proxy_pass https://my-apache-8cqci21j0m-lz.a.run.app:443; } }

Now our backend Apache server will receive the original Host header from Nginx. We still need to add some configuration in Apache so it will overwrite its Host header with the one specified in X-Forwarded-Host. You can add this to a .htaccess file or the Apache httpd.conf.

<IfModule mod_setenvif.c> SetEnvIf X-Forwarded-Host (.+) REAL_HOST_HEADER=$1 <IfModule mod_headers.c> RequestHeader set Host "%{REAL_HOST_HEADER}e" env=REAL_HOST_HEADER # Header set X-Debug-Host "%{REAL_HOST_HEADER}e" </IfModule> </IfModule>

This code will set an environment variable called REAL_HOST_HEADER , based on the request header X-Forwarded-Host . Then it will overwrite the request Host header based on the value of that environment variable. Please note that you need to have modules setenvif, and headers enabled, you can do this with the following command:

a2enmod setenvif headers

At least the latter module is not enabled by default when running the official PHP Docker image.

And that’s it! Now your backend application will receive the correct Host header from the client, even if you need to change it to a different one so that Google Cloud Run can route the request correctly.

Note: the article was updated on 2022-11-11. The previous version contained Apache config which set the Host header to an empty string if the request didn’t contain the X-Forwarded-Host header. The new code will retain the Host header sent by the browser in this case.

Comments