Nginx reverse proxy to WordPress on an URI

I have a Symfony 2.5.X app running on an nginx server. I will call it domain.com.

The /news URI within that server is configured as a reverse proxy to a remote machine, where I run WordPress blog on nginx server again. I will call it blog.domain.com.

Read More

domain.com‘s configuration looks like that:

server {
  listen 80;
  server_name domain.com;

  set $project_path /home/webserver/prod.domain.com;
  root $project_path/web;

  error_log /home/webserver/prod.domain.com/app/logs/nginx_error.log;
  access_log /home/webserver/prod.domain.com/app/logs/nginx_access.log;

  charset utf-8;
  client_max_body_size 65m;

  # Some extra speed
  open_file_cache max=1000 inactive=20s;
  open_file_cache_valid 30s;
  open_file_cache_min_uses 2;
  open_file_cache_errors on;

  # Reverse-proxy all /news calls to remote machine
  location ~ /news?(.*) {

    access_log off;

    proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
    proxy_set_header        Host                    blog.domain.com;  # without it it doesn't work
    #proxy_set_header        Host                    $http_host;
    proxy_set_header        X-Real-IP               $remote_addr;
    proxy_set_header        X-Forwarded-For         $proxy_add_x_forwarded_for;
    proxy_set_header        X-Forwarded-Proto       http;
    proxy_set_header        X-Custom-Secret         6ffe3dba7213c678324a101827aa3cf22c;

    proxy_redirect          off;
    proxy_buffering         off;
    #proxy_intercept_errors  on;

    proxy_pass http://blog.domain.com:80;
    break;
  }
  # Default URLs
  location / {
    try_files $uri /app.php$is_args$args;
  }

  # Error pages (static)
  #error_page 403                 /errorpages/403.html;
  error_page 404                  /errorpages/404.html;
  #error_page 405                 /errorpages/405.html;
  error_page 500 501 502 503 504  /errorpages/5xx.html;

  # Don't log garbage, add some browser caching
  location ~* ^.+.(ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|rss|atom|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ {
    access_log off;
    log_not_found off;
    expires max;
    add_header Pragma "public";
    add_header Cache-Control "public, must-revalidate, proxy-revalidate";
    try_files $uri /app.php?$query_string;
  }
  location ~* ^.+.(css|js)$ {
    expires modified +1m;
    add_header Pragma "private";
    add_header Cache-Control "private";
    etag on;
    try_files $uri /app.php?$query_string;
  }
  location = /robots.txt {
    allow all;
    access_log off;
    log_not_found off;
  }

  # Disallow .htaccess, .htpasswd and .git
  location ~ /.(ht|git) {
    deny all;
  }

  # Parse PHP
  location ~ ^/(app|app_dev|config).php(/|$) {
    include                 fastcgi_params;
    fastcgi_split_path_info ^(.+.php)(/.*)$;
    fastcgi_param           SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param           HTTPS           off;
    fastcgi_pass            php;
  }
}

blog.domain.com‘s configuration looks like that:

server {
  listen 80;
  server_name blog.domain.com;

  root  /home/webserver-blog/news;

  access_log    /home/webserver-blog/logs/http_access.log;
  error_log     /home/webserver-blog/logs/http_error.log;

  charset utf-8;
  client_max_body_size 65m;

  # Some extra speed
  open_file_cache max=1000 inactive=20s;
  open_file_cache_valid 30s;
  open_file_cache_min_uses 2;
  open_file_cache_errors on;

  # Default URLs
  location / {
    # This never gets parsed as / is reserved for our main server
  }
  location ~* ^/news/(wp-content|wp-admin) {  # without this directive I didn't have any static files
    root /home/webserver-topblog/;
  }
  location ~* ^/news {
    try_files $uri $uri/ /index.php?args;
  }

  # Don't log garbage
  location ~* ^.+.(ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|rss|atom|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ {
    access_log off;
    log_not_found off;
    expires max;
  }
  location = /robots.txt {
    allow all;
    access_log off;
    log_not_found off;
  }

  # Disallow .htaccess or .htpasswd
  location ~ /.ht {
    deny all;
  }

  # Disallow logs
  location ~ ^/logs/.*.(log|txt)$ {
    deny all;
  }

  # Parse PHP
  location ~ .php$ {
      #if (!-e $request_filename) { rewrite / /index.php last; }
      try_files $uri =404;

      include        fastcgi_params;
      fastcgi_index  index.php;
      fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
      fastcgi_pass   php;
  }
}

As you can figure out, my WordPress resides in /home/webserver-blog/news/. I have a slightly modified index.php file in WordPress that checks for X-Custom-Secret header, and if it’s not present (or invalid), it forces a 301 redirection to domain.com/news/

Now I have tried several different approaches to get it running properly.

  • First (and most obvious) was pointing the root of blog.domain.com‘s to /home/webserver-blog/ and allowing nginx to naturally pass the request URI to the subdirectory, /news. This worked quite well, yet it didn’t allow me to utilize WordPress’ permalinks and just worked with query strings. Other strange behaviour it produced was actually exposing blog.domain.com in HTTP redirect if you called /news without trailing slash. Those redirects were quickly handled by my custom index.php, but still I want to avoid exposing blog.domain.com completely.
  • Second (and pretty-much current) approach was again pointing the root of blog.domain.com‘s directly to WordPress’ directory, /home/webserver-blog/news/ and cheating all the requests for static files with location ~* ^/news/(wp-content|wp-admin) directive pointing it’s root directory one levelel up. This worked for both permalinks and static files, but again – /news/wp-login.php gives me infinite redirects to itself, and /news/wp-admin/ actually downloads the index.php file instead of parsing it (sends it as application/octet-stream)

I am completely out of ideas… Any help would be much appreciated.

Related posts

Leave a Reply

1 comment

  1. I think I managed to come with a so-so solution. Far from being perfect or clean, but… well, it works.

    blog.domain.com‘s config:

    server {
      listen 80;
      server_name blog.domain.com;
    
      root  /home/webserver-blog;
    
      access_log    /home/webserver-blog/logs/http_access.log;
      error_log     /home/webserver-blog/logs/http_error.log;
    
      charset utf-8;
      client_max_body_size 65m;
    
      # Some extra speed
      open_file_cache max=1000 inactive=20s;
      open_file_cache_valid 30s;
      open_file_cache_min_uses 2;
      open_file_cache_errors on;
    
      # Default URLs
      location ~* ^/news$ {
        rewrite ^ $scheme://domain.com/news/ permanent;   # ** HARDCODED production url
        break;
      }
      location / {
        try_files $uri $uri/ @redir;
      }
      location @redir {
        rewrite ^/news/(.*)$ /news/index.php?$1 last;
      }
    
      # Don't log garbage
      location ~* ^.+.(ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|rss|atom|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ {
        access_log off;
        log_not_found off;
        expires max;
      }
      location = /robots.txt {
        allow all;
        access_log off;
        log_not_found off;
      }
    
      # Disallow .htaccess or .htpasswd
      location ~ /.ht {
        deny all;
      }
    
      # Disallow logs
      location ~ ^/logs/.*.(log|txt)$ {
        deny all;
      }
    
      # Parse PHP
      location ~ .php$ {
        include                 fastcgi_params;
        fastcgi_split_path_info ^(.+.php)(/.*)$;
        fastcgi_param           SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        fastcgi_param           HTTPS           off;
        fastcgi_pass            php;
      }
    }
    

    So the trick is I’m still operating on filesystem directories and not fancy all-the-way-around rewrites and redirects. news/ still remains a physical directory in the filesystem that gets read with the location / directive from nginx. Previous issues with exposing the blog.domain.com domain on trying to access without slashes seem to be native nginx’s behaviour – it sees a directory, it adds a slash at the end; and since it’s server_name is set to blog.domain.com, here we go. Hardcoding production URL and putting that rule on top pretty much fixed the problem.

    @redir location again enabled the WordPress’ permalinks nicely.

    One more thing I have added to entire setup to prevent people form going directly on http://blog.domain.com/ is another index.php file stored directly in /home/webserver-blog/:

    <?php
    /*
     * domain.com redirector
     */
    $production = 'http://domain.com/news/';
    
    // Redirect nicely
    if(isset($_SERVER['REQUEST_URI']) and $_SERVER['REQUEST_URI'] !== '/') {
    
      $target = sprintf('%s%s', $production, preg_replace('/^//', null, $_SERVER['REQUEST_URI']));
      header('Location: ' . $target);
    }
    else header('Location: ' . $production);
    

    …and, as mentioned before, few lines on top of WordPress’ original index.php:

    <?php
    /*
     *  wordpress loader
     */
    $production = 'http://domain.com/news/';
    
    // Allow only reverse-proxied requests
    if(!isset($_SERVER['HTTP_X_CUSTOM_SECRET']) or $_SERVER['HTTP_X_CUSTOM_SECRET'] !== md5('your-md5encoded-text-in-proxy_set_header-X-Custom-Secret')) {
      die(header('Location: ' . $production));
    }
    require_once dirname(__FILE__) . '/index-wp-org.php';
    

    Ugly… but works.
    I’d still be happy to hear nicer solutons. 🙂