Proxying an application from an Nginx subdirectory back to the root of the backend server

Written by James McDonald

July 8, 2019

This is what needs to be done to get a reverse proxy working with client requesting data from Nginx at a ‘sub-directory’ location and an Apache backend serving the application from the root-directory

In hindsight this was probably what I would need to do in order to get Outlook Web Access or similar to work with NGinx as these applications embed a lot of resource URL’s that don’t play nicely with a custom or odd reverse proxy config

I learn’t not everything can be fixed with URL and request rewriting. Sometimes the response needs to be hacked too.

I have an Apache Web Server serving a CakePHP application using the CakePHP webroot as the Apache Document root as follows. A request to the Apache server for / will return the CakePHP default route.

<VirtualHost *:80>
  RewriteEngine On
  DocumentRoot /var/www/wms/app/webroot
    <Directory />
      Options +FollowSymLinks
      Options +SymLinksIfOwnerMatch
      AllowOverride All
      Order allow,deny
      AllowOverride All
      Allow from all
    </Directory>
</VirtualHost>

I then have a Nginx server acting as a reverse proxy. I want it to serve a CakePHP Application from a subdirectory location. In the following example each request for /tgn/* needs to have tgn stripped from it before it is passed to the backend

location /tgn/ {
        rewrite ^/tgn(/.*)$ $1 break;
        proxy_pass http://tgn-img/;
        proxy_set_header Accept-Encoding "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_redirect http://localhost/ http://$http_host/tgn/;
    }

The rewrite line does a good job of making sure any request to /tgn/.* gets rewritten for the back end server. The problem is the pages that get served have embedded links in the them pointing to the wrong place. So you may get an index page but all the js, css, images and other items are broken.

So you need to rewrite each response to make sure all the embedded links have /tgn/ prepended to them

Here is my nginx /etc/nginx/conf.d/default.conf file. Showing examples of using the http_sub_module to find and replace values to make sure the pages request resources from the correct URL. Note the comments.

Check you have the http_sub_module by running nginx -V and looking for --with-http_sub_module in the output.

Create sub_filter rules with a find and replace snippets of anything in the HTML pages that would need changing

server {

  listen 80;
  server_name localhost;
  # sub_filter_types '*';
  #charset koi8-r;
  access_log /var/log/nginx/host.access.log main;
  location /tgn/ {

    # these sub_filter rules
    # scan the text/html response from the backend
    # and find and replace them to make them work
    # with the above location
    # you need to use your dev tools in your browser
    # and find all the 404's and other weird errors
    # and then track them down and add a rule here

    # all the image src elements, css & js sources
    # need changing
    sub_filter 'src="/files/' 'src="/tgn/files/';
    sub_filter '<link href="/img/'  '<link href="/tgn/img/';
    sub_filter 'href="/css/' 'href="/tgn/css/';
    sub_filter 'src="/img/' 'src="/tgn/img/';
    sub_filter 'src="/js/' 'src="/tgn/js/';


    # embedded data-* elements need changing
    sub_filter 'data-queryurl="/items/' 'data-queryurl="/tgn/items/';

    # Form Submit actions need re-writing
    sub_filter 'action="/Labels' 'action="/tgn/Labels';

    # Links to CakePHP Controllers re-writtern
    sub_filter 'href="/Labels' 'href="/tgn/Labels';
    sub_filter 'href="/Shipments' 'href="/tgn/Shipments';
    sub_filter 'href="/PrintLabels' 'href="/tgn/PrintLabels';
    sub_filter 'href="/Items' 'href="/tgn/Items';
    sub_filter 'href="/Locations' 'href="/tgn/Locations';
    sub_filter 'href="/Menus' 'href="/tgn/Menus';
    sub_filter 'href="/Users' 'href="/tgn/Users';
    sub_filter 'href="/Settings' 'href="/tgn/Settings';
    sub_filter 'href="/ProductTypes' 'href="/tgn/ProductTypes';
    sub_filter 'href="/Shifts' 'href="/tgn/Shifts';
    sub_filter 'href="/PrintTemplates' 'href="/tgn/PrintTemplates';
    sub_filter 'href="/LabelHistories' 'href="/tgn/LabelHistories';
    sub_filter 'href="/PackSizes' 'href="/tgn/PackSizes';

    # code injection too!
    sub_filter '</head>' '<script>/path/to/code.js</script></head>';

    # if you don't turn this off
    # the http_sub_module will only replace
    # the first instance it finds
    # this makes the find and replace global
    sub_filter_once off;

    # this rewrite rule is the core of getting nginx
    # to serve a backend application from a subdirectory
    # requests to nginx at /tgn/ goto the backend at /
    rewrite ^/tgn(.*)$ $1 break;
    # the break command says once you do this
    # rewrite continue on with the next
    # directives inside this block. i.e. 
    # pass the request to the backend
    #
    # if break was replaced with 'last'
    # processing would stop and exit the location
    # block and then try and rematch the 
    # request to a location. See the docs I'm
    # spit balling here.

    # since I was using docker containers
    # tgn-img is the name of the Apache container
    # which held the CakePHP app
    proxy_pass http://tgn-img;


    # ***** IMPORTANT *******
    # without this Accept-Encoding the Apache Server
    # sends back compressed content and the sub_filter
    # statements don't work
    proxy_set_header Accept-Encoding "";
    proxy_set_header Host $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 https;

    # I found Cake was issuing a redirect in code
    # $this->redirect([
    #       'action' => 'pallet_print', 'marg'
    # ]);
    # and the Location: headers pointed to the
    # wrong place a CakePHP redirect would emit something like
    # Location: http://localhost/Labels/pallet_print/marg
    # The following
    # proxy_redirect statement would change it from the
    # above to http://localhost:34562/tgn/Labels/pallet_print/marg
    # which was correct!

    # note the $http_host variable this is the name and port the
    # client sees in the browser address bar
    #
    # when using docker containers
    # the server process (nginx, apache) can be running
    # on a default port e.g. 80 and then be
    # published to the outside world as a fixed or semi-random
    # non-standard port my nginx container published itself
    # in the thirty thousands 32840 $http_host captures what the client
    # sees.
    proxy_redirect http://localhost/ http://$http_host/tgn/;
  }

  location / {
    # without doing the sub_filters requests will hit this
    # block and miss the backend entirely

    # This redirect shunts anything short of /tgn/ to /tgn/
    # you may not want this behavior
    return 301 $scheme://$http_host/tgn/;
  }

  #error_page  404              /404.html;

  # redirect server error pages to the static page /50x.html
  #
  error_page 500 502 503 504 /50x.html;
  location = /50x.html {

    root /usr/share/nginx/html;
  }

  # proxy the PHP scripts to Apache listening on 127.0.0.1:80
  #
  #location ~ \.php$ {
  #    proxy_pass   http://127.0.0.1;
  #}

  # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
  #
  #location ~ \.php$ {
  #    root           html;
  #    fastcgi_pass   127.0.0.1:9000;
  #    fastcgi_index  index.php;
  #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
  #    include        fastcgi_params;
  #}
  # deny access to .htaccess files, if Apache's document root
  # concurs with nginx's one
  #
  #location ~ /\.ht {
  #    deny  all;
  #}
}


0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

The reCAPTCHA verification period has expired. Please reload the page.

You May Also Like…

Squarespace Image Export

To gain continued access to your Squarespace website images after cancelling your subscription you have several...

MySQL 8.x GRANT ALL STATEMENT

-- CREATE CREATE USER 'tgnrestoreuser'@'localhost' IDENTIFIED BY 'AppleSauceLoveBird2024'; GRANT ALL PRIVILEGES ON...

Exetel Opt-Out of CGNAT

If your port forwards and inbound and/or outbound site-to-site VPN's have failed when switching to Exetel due to their...