In this video, we work through how to put your PHP application in a subdirectory of another site.
For example, we may have an application running at example.org
but need a second application running at example.org/blog
.
This feels like it should be simple, but it turns out to be more complex and fraught with confusing Nginx configurations! To make matter worse (or, perhaps, to illustrate this point), a quick Google search reveals a TON of confusing, non-working examples.
What We're Using
The server is Ubuntu 16.04, , we install Nginx 1.13 and PHP 7.2. The example PHP applications are Laravel 5.5.
TL;DR
Here's the working configuration to have two Laravel apps working, where one application exists in a subdirectory of another.
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/top/public;
index index.html index.htm index.php;
server_name _;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location /nested {
alias /var/www/nested/public;
try_files $uri $uri/ @nested;
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_param SCRIPT_FILENAME $request_filename;
fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
}
}
location @nested {
rewrite /nested/(.*)$ /nested/index.php?/$1 last;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
}
}
How This Works
Let's cover some details of the above configuration to see what's going on.
Project Locations
In this example, the application files of the two applications don't actually exist within one another. The application top
exists in /var/www/top
while the "nested" application lives in /var/www/nested
.
This is, in my opinion, the prefered setup in this case, as nesting application files where you "physically" put one project's files inside of another's gets really messy with most PHP applications' use of a public
directory as a web root.
Your case may change a bit depending on the applications of course, but that shouldn't change too much of this guide.
The Basic Setup
We start with a basic server configuration, good for serving a single application:
server {
listen 80 default_server;
root /var/www/top/public;
index index.html index.htm index.php;
server_name _;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
}
}
This excludes some stuff you might want in a production app (e.g. cache headers for static assets), but is enough to get a PHP application working fine.
The relevant parts to this guide are:
root
- we define the web root that is used with applicationtop
index
- We defineindex.php
as one of the default files to look for if no file is specified in the URIlocation { try_files ... }
- We use the standardtry_files
directive in a block that catches all URI's. This looks to see if a file or directory sent in the URI exists within theroot
directory. If it does not exist, it goes to theindex.php
file, which we know for sure is in theroot
directory.
Any part of the URI is appended to the
root
directive. So a URI of/foo/bar
will search for a file or directory/var/www/top/public/foo/bar
. In combination with theindex
directive, it also seaches for/var/www/top/public/foo/bar/index.[html|htm|php]
.
More interestingly however, the try_files
directive will send most requests (except those for static assets - js, css files) to /index.php
. Just like with any other URI, that is getting appended to the root
, so Nginx will look for /var/www/top/index.php
.
The last location ~ \.php$ {...}
block is used for any files ending in .php
. It sends the file off to PHP-FPM for processing.
Adding an App in a Subdirectory
We run into a few issues when running a second app within a subdirectory. Let's walk through them.
First, we know we want a second application to run in subdirectory /nested
(the subdirectory is arbitrary, I just chose to name it "nested"). So, we create a location
block for the "nested" location:
location /nested {
}
Now we can add to it to get our application running.
web root:
We know we have a separate web root for the nested
project. Like me, your first inclination may be to set a root
to point Nginx to /var/www/nested/public
, but this doesn't end up working! Instead, we need to use an alias
:
location /nested {
# We point the alias to the "nested"
# project's web root
alias /var/www/nested/public;
}
An alias
is similar to root
, but it has different behavior.
Nginx combines the root
+ the given URI to find a file on the disk drive. For example, a URI /foo/bar
will instruct Nginx to serve /var/www/top/public/foo/bar
. Since that directory doesn't exist, the try_files
part tells Nginx to send the request to /var/www/top/public/index.php
.
The included
snippets/fastcgi-php.conf
configuration takes care of parsing out/foo/bar
as the intended URI that the PHP application uses as a route.
The alias
does not behave exactly like this. In our example, the /nested
URI is aliased to /var/www/nested/public
. A URI of /nested
will therefore look in /var/www/nested/public
for a file to serve. A URI of /nested/foo
will instruct Nginx to look in /var/www/nested/public/foo
; the /nested
portion is omitted.
Note that distinction - with that alias, Nginx does NOT look for files within /var/www/nested/public/nested/foo
, like it would with the root
directive.
try_files
:
So, alias
has the behavior we want - it won't try to look for files inside of some nested
directory that does not exist. We still need to make a URI like /nested/foo
serve from file /var/www/nested/public/index.php
, so we have to add another try_files
.
location /nested {
alias /var/www/nested/public;
try_files $uri $uri/ /index.php$is_args$args;
}
That looks very familiar! However, we're going to have to do some more PHP things specifically in the /nested
location, so let's get a PHP handler in there also:
location /nested {
alias /var/www/nested/public;
try_files $uri $uri/ /index.php$is_args$args;
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_param SCRIPT_FILENAME $request_filename;
fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
}
}
The PHP handler location
block is nested within the location /nested {...}
block so we can customize it a bit.
Note that I snuck something into the PHP location
block there. I (re)set the SCRIPT_FILENAME
FastCGI parameter to $request_filename
. This normally is $document_root$fastcgi_script_name
, which in our configuration would always point to /var/www/top/public/index.php
- the wrong application!
Instead we use $request_filename
, which has the following definition in the Nginx docs:
file path for the current request, based on the root or alias directives, and the request URI
So this takes the alias
into account correctly, and will pass the correct path to the index.php
file in our nested application.
It works?!
This will appear to work at first! Heading to URI /nested
will work fine. What happens is /nested
is aliased to /var/www/nested/public
and try_files
sends that to /index.php
, which ends up correctly being /var/www/nested/public/index.php
.
However, if our app has a defined route (foo
, for example), and we try to head to URI /nested/foo
to reach it, we receive a 404 error! Not only that, it's a 404 page from the top level app!
This request ends up being handled by the outer PHP handler, which uses $document_root$fastcgi_script_name
for the SCRIPT_FILENAME
, thus pointing to file /var/www/top/public/index.php
and being served by the top level app.
I've confirmed and tested the request is handled by the
location /nested
block, and is then sent to the wronglocation ~ \.php$ {...}
block. I'm not sure why this happens though! If you do, drop me a tweet @fideloper or email -chris
AT this site's domain.
To get around this issue, we rewrite
the URI to put index.php
in the correct spot in the eyes of Nginx.
rewrite
:
To implement the rewrite, we'll use a named location:
location /nested {
alias /var/www/nested/public;
try_files $uri $uri/ @nested;
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_param SCRIPT_FILENAME $request_filename;
fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
}
}
location @nested {
rewrite /nested/(.*)$ /nested/index.php?/$1 last;
}
We adjust try_files
to send requests to @nested
, and then define @nested
as a named location
block.
This location block rewrites the URI so /nested/foo
becomes /nested/index.php/foo
. The last
directive tells Nginx to use the new, rewritten URI and match it against the other location
blocks.
Nginx then re-reaches the location /nested {...}
block, where it's parsed correctly! The index.php
file is in the correct location on the disk, and the included fastcgi-php.conf
configurations parse out the foo
route from the rest of the URI as normal (via fastcgi_split_path_info
, if you were curious).
Don't forget the needed change to the
SCRIPT_FILENAME
FastCGI parameter explained above as well. That sends our correctly-parsed script file path to PHP-FPM.
Testing
Here's what I did to create this scenario and test out the Nginx configuration:
# Ubuntu 16.04 Server
sudo add-apt-repository -y ppa:ondrej/php
sudo add-apt-repository -y ppa:nginx/development
sudo apt-get update
sudo apt-get install -y nginx php7.2-fpm php7.2-cli \
php7.2-pgsql php7.2-sqlite3 php7.2-gd \
php7.2-curl php7.2-memcached \
php7.2-imap php7.2-mysql php7.2-mbstring \
php7.2-xml php7.2-zip php7.2-bcmath php7.2-soap \
php7.2-intl php7.2-readline php7.2-imagick php-msgpack php-igbinary
php -r "readfile('http://getcomposer.org/installer');" | sudo php -- --install-dir=/usr/bin/ --filename=composer
cd /var/www
sudo composer create-project laravel/laravel top
sudo composer create-project laravel/laravel nested
sudo chown -R www-data: top nested