Clean-URL rewrite rule with nested "location" and alias directive

Ben Johnson ben at
Sat Nov 23 17:36:55 UTC 2013

On 11/21/2013 3:45 PM, Francis Daly wrote:
> On Wed, Nov 20, 2013 at 10:31:07PM -0500, Ben Johnson wrote:
>> On 11/20/2013 4:10 AM, Francis Daly wrote:
> Hi there,
>>> If you do have free choice in the matter, some things work more easily
>>> within nginx if you can use "root" and not "alias" -- so if you want
>>> files to be accessible below the url /stage/, having them directly in
>>> a directory called /stage/ is convenient.
>> a.) I prefer to maintain identical directory structures within my
>> staging and production environments. This allows my build scripts and
>> relative paths to remain identical (provided that I set a "root path"
>> variable or similar in each shell script), and it helps me to remember
>> to where I must "cd" in order to execute a build script.
> That's perfectly sensible.
> And the web server configurations for production and staging would
> also be very similar, if they were served at similar points in the
> hierarchy of the web domain or domains.
> But because you serve one at "/" and one at "/stable/", you end up
> having different configurations. And because of nginx's implementation
> of "alias", and your use of a filesystem hierarchy which means you need
> "alias", you end up having very different configurations.

I see; this seems to be the crux of my struggle. :)

>> b.) I really prefer to keep PHP and PHP-template files out of the
>> document root (on the filesystem).
> That seems mostly unrelated, unless I'm missing something. Put your php
> files in "php/" which is parallel to "web/" if you like, nginx won't
> care. In fact, if you don't use try_files, nginx won't even look. All
> it needs to know is what filename to tell the fastcgi server to read.
>> I don't want user-agents to be able
>> to request .tpl files directly, for example, by requesting
>> /templates/home.tpl.
> I confess that I thought that was standard -- the php file itself should
> have written in it "read your associated files from this directory
> location", which should not be web-accessible (unless you specifically
> want it to be).

It *should* be standard, but consider frameworks such as WordPress,
Joomla, Drupal, etc. They all require (or at least assume) everything to
be within the virtual host's document root (the "web" directory, per our
discussion here). Not directly relevant; just an observation that we
seem to be "ahead of the pack" in this best-practice.

>> course, I build my applications such that every request is routed
>> through a single point that has complete authority over how the request
>> is handled.
> If you really mean "every", then the only nginx config you need is
>   location ^~ /myapp/ {
>     include fastcgi_params;
>     fastcgi_param SCRIPT_FILENAME /var/myapp.php;
>     fastcgi_pass unix:php.sock;
>   }
> Anything else is the web server getting in the way (and implementing
> things that it can probably do more efficiently than your application
> can).

You're right; I don't mean *every* request -- I mean only those that are
not for a "real file" and that match the format of a resource that I may
serve via PHP.

> So the time spent arranging the web server configuration you want can
> be counted as time saved not implementing the extra features in your
> application.
>> In the second case, this seems risky; if I were to put the "stage"
>> directory within the production site's document root,
> I may have been unclear, or I may be misunderstanding you now.
> What I intended to suggest was that: currently all of
> your staging web content is visible on the filesystem in
> /var/www/;
> if you either move or copy (or possibly even symlink) it to be visible
> in /var/www/, then you will be able
> to configure nginx using "root" instead of "alias", and your "staging"
> configuration can look much more like your "production" one.

Yes, this was a simple misunderstanding; I thought you meant to move
everything to /var/www/ Thanks for clarifying
this point.

This seems to be "the hot ticket". I have no reason for not doing this.

>       location ^~ /stage/ {
>         root /var/www/;
>         # The files are read from /var/www/
>         index index.php index.html index.htm;
>         try_files $uri $uri/ /stage/index.php?q=$uri;
>         location ~ \.php$ {
>           # use a different "root" here if you want; but make sure the php
>           # files can be read from within "stage/" below that root.
>           try_files $uri /stage/index.php?q=$uri;
>           fastcgi_pass unix:/var/run/php5-fpm.sock;
>           fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
>           include /etc/nginx/fastcgi_params;
>         }
>       }
>> I meant to say that there are certain cases in which I want index.php to
>> dispatch the request to dedicated CSS and JS controllers.
> If "dispatch to the controller" means "something within php, and therefore
> nginx doesn't have to know or care about it", then it probably should
> Just Work.
>> Maybe it would be helpful for me to demonstrate how I achieve this when
>> the "site" is not accessed in a subdirectory (with respect to the URL)
>> and the files actually exist in the virtual host's document root.
>> location / {
>> 	try_files $uri $uri/ @virtual;
>> }
> That'll become "/stage/" and "@stagevirtual", I guess. But it'll probably
> want a suitable "root" added -- your "location /" can safely inherit
> the server one, while "/stage/" can't.
>> location @virtual {
>> 	fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
> @stagevirtual will need to know its version of $document_root. So set
> a suitable "root" there too.
>> 	if ($uri ~ '/download/.*/$') {
>> 		rewrite ^/(.*)/$ /$1 permanent;
> "/stage/" will already be there in both parts of the rewrite, so no
> change needed.
>> 	if ($uri !~ '(/|\.[a-zA-Z0-9]{1,12}|/download/.*)$') {
>> 		return 301 $uri/$is_args$args;
> "/stage/" will already be there in $uri, so no change needed.
>> 	if ($uri ~ '(/|/download/.*|/css2/.*|/js2/.*)$') {
>> 		rewrite  ^(.*)$  /index.php?q=$1  last;
> That should become "/stage/index.php".
> (Note that in this specific rewrite, $1 == $uri, so you could avoid the
> match-and-capture.)
>> This is all I'm trying to do. I just want to replicate the above with
>> the "less-friendly" directory structure that we've been discussing.
> I'm not seeing many changes being needed. Provided you avoid
> "alias". Which requires a filesystem change. Which hopefully won't break
> the non-nginx part of your workflow.
> (Actually, within your @stagevirtual, the only place root/alias seems to
> matter is in the SCRIPT_FILENAME thing. So if you can build your own from
> the variables you have to hand, you can probably get away with whatever
> file structure you like.)
>> If there is a better, more concise, or more secure way to accomplish the
>> above, then I am eager to learn.
> In your staging web directory: ln -s . stage. In your staging php
> directory: ln -s . stage. Then set "root" in your "location ^~/stage/"
> and in your "location @stagevirtual".
> So long as everything that tries to recurse into the directories can
> recognise the symlink loop, it should be fine.
> Then in production, either "rm stage", or don't create the link in the
> first place.

Okay, I'm trying to implement this, but something seems to have gone
terribly awry. I apologize for derailing our progress, but until this is
resolved, I have no way to test your recommendations.

It's bizarre. At some point while meddling with the configuration,
requests for /stage/ began causing the browser to download index.php
(and I can open the file and see the PHP code). And no matter what I
change in the configuration, this behavior persists.

I went so far as to rename the "stage" directory to "staging", and
changed all references in nginx's configuration accordingly. Yet, for
some reason, requests for /stage/ still download the index.php file! I'm
not even sure where this index.php file is coming from, given that nginx
should have absolutely no knowledge of this file's new/current location.

The weirdest part is that the downloaded file is *not* the file that
exists at /index.php (the "main site" index file). I confirmed this by
adding some commented PHP code at the bottom of /index.php, and the
comments do not appear in the downloaded file.

Hell, I even tried deleting the entire "staging" directory and this
still happens! I've restarted nginx, php5-fpm, etc. and nothing changes.
Where is this file coming from???

>> Hmm, I think I see what you're getting at here. But is there no means by
>> which to achieve the desired configuration with my particular directory
>> structure? I'm a bit surprised at how difficult it is to decouple the
>> request location from the filesystem directory from which it is served,
>> while maintaining this clean-URL setup.
> If you want nginx, and you want a directory structure that requires
> nginx's "alias", then you get to deal with nginx's "alias", which has
> some imperfections.
> Change one of your wants, and the problem disappears.
> All the best,
> 	f

Thanks again for seeing me through this, Francis. A bottle of your
favorite spirit is in order once this whole affair is resolved!


More information about the nginx mailing list