Redirect loop with Wordpress on Apache with nginx reverse proxy and HTTPS on Ubuntu 16

Posted May 4, 2017 56.9k views
NginxApacheWordPressUbuntu 16.04

I’m experimenting with a DO droplet to host my Wordpress blog, setting it up myself because I’m difficult that way.

I configured Apache with TLS via Lets Encrypt and migrated my Wordpress blog over, and everything was working fine.

I then put nginx in front of Apache as a reverse proxy and configured nginx as the TLS terminator, redirecting any non-https to https using the tutorials here.

Now I can access any static files which are served up by nginx, and accessing a phpinfo() test file on Apache works fine. But accessing any Wordpress php files, like index.php or admin triggers a redirect loop.

What did I do wrong? Here’s my nginx config

# Default server configuration
server {
        listen 80;
        listen [::]:80;

        # redirect all http to https
        return 301 https://$server_name$request_uri;

server {
        # SSL configuration
        listen 443 ssl http2 default_server;
        listen [::]:443 ssl http2 default_server;
        include snippets/;
        include snippets/ssl-params.conf;

        root /var/www/;

        index index.php index.html index.htm;


        location / {
                try_files $uri $uri/ /index.php;

        # proxy PHP requests to Apache
        location ~ \.php$ {
                proxy_set_header X-Real-IP  $remote_addr;
                proxy_set_header X-Forwarded-For $remote_addr;
                proxy_set_header Host $host;

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one

        location ~ /\.ht {
                deny all;


These answers are provided by our Community. If you find them useful, show some love by clicking the heart. If you run into issues leave a comment, or add your own answer to help others.

Submit an Answer
5 answers


The only issue with your server block that I can see is within the location block that handles PHP.

        location ~ \.php$ {
                proxy_set_header X-Real-IP  $remote_addr;
                proxy_set_header X-Forwarded-For $remote_addr;
                proxy_set_header Host $host;

Specifically, this line:


You shouldn’t need to add $request_uri to the end of the proxy, so my first recommendation would be to remove $request_uri from the URL and leave it as:


Another issue is most likely due to termination of SSL. WordPress doesn’t handle proxies all that well and from what I’ve read, have no intention on implementing anything to make it easier, so you may need to add a bit of code to your wp-config.php file.

Open wp-config.php and find:

define('WP_DEBUG', false);

Directly below it, add:

if ( $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' )
    $_SERVER['HTTPS']       = 'on';
    $_SERVER['SERVER_PORT'] = 443;

Close and save the file, then refresh the page and see if you’re still seeing the loop.

If yes, and you happen to be using CloudFlare, there’s a fix for that too :-). CloudFlare needs to be set to use Full (Strict) under the SSL/Crypto menu (should be the first option). Without Full (Strict), you end up with a loop that seems hopeless.

If none of that works, my first recommendation would be to check the error logs.

tail -50 /var/log/nginx/error.log

Please paste the output in to a code block as a reply.

My second recommendation, ditch Apache as you don’t need it, even for WordPress (or any other CMS for that matter). NGINX + PHP-FPM can handle more than Apache can with mod_php and the setup is pretty darn simple (I’ll be more than happy to help if you want to go that route).

  • Thanks for the help, jtittle.

    Main reason I’m trying Apache+nginx is I’d read pure nginx+fpm turns all your wordpress permalinks into something like “” and besides being ugly I was concerned about problems caching since the URL would have a query string in it.

    I’m not using Cloudflare at the moment, wanted to get this running smoothly with https before I got complicated. Also, still undecided if I want to trade CDN for proxying my secure connections… Was hoping leverage nginx speed plus its cache on a VPS to be fast enough to reduce the need for the CDN.

    Of course, I was also expecting Apache+nginx to be simpler than pure nginx+fpm but didn’t anticipate this redirect loop either…

    I tried with and without the $request_uriin proxy_pass, no difference. Same with modifying the wp-config.php. Restarted apache and ngninx after each change.

    No errors in the nginx error.log and apache error.log just shows the service stopping and starting. Access logs just show the redirect loop.

    Do I need to modify my nginx proxy headers to include the request method for the wp-config.php conditional you suggested to work? I’m doing http between Apache and nginx, so I expect Wordpress sees the traffic as http from nginx.

    edited by kamaln7
    • @jackl

      Actually, NGINX + PHP-FPM can be as simple as:

          listen 80;
          listen [::]:80;
          root /home/nginx/htdocs/public;
          location /
              try_files $uri $uri/ /index.php?$args;
          location ~ [^/]\.php(/|$) {
              fastcgi_split_path_info ^(.+?\.php)(/.*)$;
              fastcgi_pass unix:/run/php/php7.1-fpm.sock;
              fastcgi_index index.php;
              include /etc/nginx/config/php/fastcgi_params;

      The above works for WordPress and is the NGINX + PHP-FPM solution. The fastcgi_params file contains:

      fastcgi_connect_timeout 60;
      fastcgi_send_timeout 180;
      fastcgi_read_timeout 180;
      fastcgi_buffer_size 512k;
      fastcgi_buffers 512 16k;
      fastcgi_busy_buffers_size 1m;
      fastcgi_temp_file_write_size 4m;
      fastcgi_max_temp_file_size 4m;
      fastcgi_intercept_errors off;
      fastcgi_param SCRIPT_FILENAME   $request_filename;
      fastcgi_param PATH_INFO         $fastcgi_path_info;
      fastcgi_param PATH_TRANSLATED   $document_root$fastcgi_path_info;
      fastcgi_param QUERY_STRING      $query_string;
      fastcgi_param REQUEST_METHOD    $request_method;
      fastcgi_param CONTENT_TYPE      $content_type;
      fastcgi_param CONTENT_LENGTH    $content_length;
      fastcgi_param SCRIPT_NAME       $fastcgi_script_name;
      fastcgi_param REQUEST_URI       $request_uri;
      fastcgi_param DOCUMENT_URI      $document_uri;
      fastcgi_param DOCUMENT_ROOT     $document_root;
      fastcgi_param SERVER_PROTOCOL   $server_protocol;
      fastcgi_param REQUEST_SCHEME    $scheme;
      fastcgi_param HTTPS             $https if_not_empty;
      fastcgi_param HTTP_PROXY        "";
      fastcgi_param GATEWAY_INTERFACE CGI/1.1;
      fastcgi_param SERVER_SOFTWARE   nginx/$nginx_version;
      fastcgi_param REMOTE_ADDR       $remote_addr;
      fastcgi_param REMOTE_PORT       $remote_port;
      fastcgi_param SERVER_ADDR       $server_addr;
      fastcgi_param SERVER_PORT       $server_port;
      fastcgi_param SERVER_NAME       $server_name;
      fastcgi_param REDIRECT_STATUS   200;

      The only changes you’d need to make are file and directory permissions. By default, PHP will run as www-data, so all files and directories need to be owned by the user, otherwise PHP won’t be able to write (which would prevent you from uploading themes, creating cached files, etc).

      For example, I’m using /home/nginx/htdocs/public as the home directory in the above server block, so I’d run:

      chown -R www-data:www-data /home/nginx/*

      That fixes all the user:group permissions on all files and directories in one command.

      As for using the proxy, I normally use a slightly more detailed configuration, which includes the header defined in the code. My proxy configuration normally looks like:

          proxy_buffers 16 32k;
          proxy_buffer_size 64k;
          proxy_busy_buffers_size 128k;
          proxy_cache_bypass $http_pragma $http_authorization;
          proxy_connect_timeout 59s;
          proxy_hide_header X-Powered-By;
          proxy_http_version 1.1;
          proxy_ignore_headers Cache-Control Expires;
          proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504 http_404;
          proxy_no_cache $http_pragma $http_authorization;
          proxy_pass_header Set-Cookie;
          proxy_read_timeout 600;
          proxy_redirect off;
          proxy_send_timeout 600;
          proxy_temp_file_write_size 64k;
          proxy_set_header Accept-Encoding '';
          proxy_set_header Cookie $http_cookie;
          proxy_set_header Host $host;
          proxy_set_header Proxy '';
          proxy_set_header Referer $http_referer;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header X-Forwarded-Host $host;
          proxy_set_header X-Forwarded-Server $host;
          proxy_set_header X-Real-IP $remote_addr;
          proxy_set_header X-Forwarded-Proto $scheme;
          proxy_set_header X-Original-Request $request_uri;

      In your case, to use the above, you’d keep proxy_pass; and then replace the lines you have with the above, and then restart NGINX.

      • Thanks again, jtittle!

        That may look simple to you, but to an nginx novice like me, it’s complicated :-)

        I was just about to give up, but did manage to get it working. I’m not sure what exactly was the magic that made it work, but it was a combination of modifying the wp-config.php and the requesturi variable.

        Here’s what ultimately broke the redirect loop and got Wordpress talking with Apache and nginx.

        nginx config

                location ~ \.php$ {
                        proxy_set_header X-Real-IP $remote_addr;
                        proxy_set_header X-Forwarded-For $remote_addr;
                        proxy_set_header X-Forwarded-Host $host;
                        proxy_set_header Host $host;
                        proxy_set_header X-Forwarded-Proto $scheme;


        if ( $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' )
                $_SERVER['HTTPS']       = 'on';
                $_SERVER['SERVER_PORT'] = '443';
                define('FORCE_SSL_ADMIN', true);
        if ( isset($_SERVER['HTTP_X_FORWARDED_HOST'])) {

        I suspect I can remove some of the items in wp-config, like FORCESSLADMIN but it works for now, so on to tweaking caching.

        Frankly, I’m a bit disappointed in the performance so far. Granted there is some room for tuning my nginx and apache config but I was expecting much faster page load times than I’m seeing.

        • @jackl

          That may look simple to you, but to an nginx novice like me, it’s complicated :-)

          That’s why the community exists :-). I’m pretty active here and there are others that drop in as well – we all try to help as much as possible.

          The example I provided for NGINX + PHP-FPM is very basic and barebones, but it’s what’s needed for WordPress. Try not to focus so much on how complex it may be at first glance. There’s a lot that I refer to the docs on, even as a sysadmin. It’s not all that easy to remember every single variable across multiple servers and various languages. Trying to will just give you a migraine :-).

          Take it a step at a time – in doing so, what may not make sense now will begin to.

          In regards to performance, you’re still offloading dynamic requests to Apache and mod_php, which is resource hungry and relatively slow unless you spend a bit of time tweaking/tuning. Even then, it can still be slower.

          When it comes to tweaking/tuning, however, there’s more at play than just the web server. You also have to factor in MySQL/MariaDB for the database as well as what you’re using on the backend for WordPress.

          More plugins normally equates to more resources being used.

          You also have to consider that more HTTP requests means slower page load. Even with NGINX serving static content, you still have to make sure static is actually static. That means checking for query strings on files.

          More often than not, WordPress appends version numbers to files, like so:


          The above is just an example, but it’s an example that isn’t cacheable. You’d need to remove that query string for jquery.js to be fully cacheable and subject to the expiration times that may be set.



          There’s a lot that goes in to performance tuning and it’s two-sided :-).

        • @jackl I had exactly the same symptoms, and your solution appears to be the minimal steps to fix it. I tried removing some of the items in wp-config, but the loop continued. It only stopped when I used your solution exactly.

          • Hey, I know this is a year late, but I have spent the past 10 hours struggling with that.
            I have found the issue (infinite loop) to be caused by having the wordpress setup with https urls and running on Docker using the http protocol.
            So a proxy passing a request to the wordpress instance proxy_pass is sending traffic over http, wordpress oddly enough redirects the users to match the url in the config, which is https in my scenario.

        • @jack!
          Having pounded my head flat for 16 hours, your solution solved the problem. Many thanks!

actually i had setup my ubuntu server with vesta cp (apache with nginx proxy). i installed letsencrypt and it installed successfully. the problem is ssl works only with vestacp admin panel on port 8083. other than that document root (public_html) never works. I tried all possible fixes googling and it never worked. i checked everything. port 443 is open. but still not loading.

the error i get in firefox is : “Secure Connection Failed, The connection to **.com was interrupted while the page was loading. The page you are trying to view cannot be shown because the authenticity of the received data could not be verified.”

none of my firewall configuration blocking it. i have removed and reinstalled letsencrypt certificates using certbot successfully but the same thing happens again.
there are two files created by vestacp for nginx config. one is for normal http “nginx.conf” and another one is for https “snginx.conf”
my nginx.conf has the following codes:

server {
    ssl         on;
    ssl_certificate      /home/admin/conf/web/;
    ssl_certificate_key  /home/admin/conf/web/;
    error_log  /var/log/apache2/domains/ error;

    ### Add SSL specific settings here ###

    ssl_protocols        SSLv3 TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers RC4:HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;
        keepalive_timeout    60;
    ssl_session_cache    shared:SSL:10m;
        ssl_session_timeout  10m;

    location / {
        location ~* ^.+\.(jpeg|jpg|png|gif|bmp|ico|svg|tif|tiff|css|js|htm|html|ttf|otf|webp|woff|txt|csv|rtf|doc|docx|xls|xlsx|ppt|pptx|odf|odp|ods|odt|pdf|psd|ai|eot|eps|ps|zip|tar|tgz|gz|rar|bz2|7z|aac|m4a|mp3|mp4|ogg|wav|wma|3gp|avi|flv|m4v|mkv|mov|mpeg|mpg|wmv|exe|iso|dmg|swf)$ {
            root           /home/admin/web/;
            access_log     /var/log/apache2/domains/ combined;
            access_log     /var/log/apache2/domains/ bytes;
            expires        max;
            try_files      $uri @fallback;

    location /error/ {
        alias   /home/admin/web/;

    location @fallback {

    location ~ /\.ht    {return 404;}
    location ~ /\.svn/  {return 404;}
    location ~ /\.git/  {return 404;}
    location ~ /\.hg/   {return 404;}
    location ~ /\.bzr/  {return 404;}

    include /home/admin/conf/web/*;

the only modification i made here was code in “### Add SSL specific settings here ###”
.i checked nginx config and restarted it was ok. but still not working.

there are two files created by vestacp for apache config. one is for normal http “apache2.conf” and another one is forhttps “sapache2.conf”
my sapache2.conf file has following code in it


    DocumentRoot /home/admin/web/
    ScriptAlias /cgi-bin/ /home/admin/web/
    Alias /vstats/ /home/admin/web/
    Alias /error/ /home/admin/web/
    SuexecUserGroup admin admin
    CustomLog /var/log/apache2/domains/ bytes
    CustomLog /var/log/apache2/domains/ combined
    ErrorLog /var/log/apache2/domains/

    SSLEngine on
    SSLVerifyClient none
    SSLCertificateFile /home/admin/conf/web/
    SSLCertificateKeyFile /home/admin/conf/web/
    SSLCertificateChainFile /home/admin/conf/web/

    <Directory /home/admin/web/>
        AllowOverride All
        Options +Includes -Indexes +ExecCGI
        php_admin_value open_basedir /home/admin/web/
        php_admin_value upload_tmp_dir /home/admin/tmp
        php_admin_value session.save_path /home/admin/tmp
    <Directory /home/admin/web/>
        AllowOverride All

    <IfModule mod_ruid2.c>
        RMode config
        RUidGid admin admin
        RGroups www-data
    <IfModule itk.c>
        AssignUserID admin admin

    IncludeOptional /home/admin/conf/web/*


i tried reloading and restarting apache and nginx. it runs ok but https only works on port 8083, vestacp admin panel. i tried disabling firewalls and checked. the result is same.

all i can see is in chrome it reloads several times like establishing secure connection, connecting and finaly error follows after few seconds. in firefox, error as said above.
i checked by placing a dummy index.html in my home directory (moving wordpress index.php). but same error comes.

please help me. i am cracking my head here..

@digitalocean would be great to have a one-click LEMP/LAMP stack on Ubuntu 18.04 with nginx running on the front side of apache. Like what serverpilot offers.

  • Most control panels with auto installers essentially do exactly that…install a lamp/lamp stack

    Virtualmin, vestacp, centos web panel etc.

Also I noticed that a reply was given suggesting disappointing results with adding nginx to apache. There was mention of mod php without explaining this further.

Might I suggest checking to see whether or not you are running mpm prefork (which is default) or mpm worker or mpm event.

My understanding is that mpm prefork is a bit of a resource hog, so “mpm worker” and “mpm event” are the preferred option if the system is struggling a bit.