Need help to create mobile & desktop website with nginx

We encourage users to post events happening in the community to the community events group on https://www.drupal.org.
spacereactor's picture

Currently most detail guide focus on creating mobile website with sub-domain like m.website.com for mobile site and website.com or www.website.com as desktop site cause is easier to cache by domain but also include apache rewrite and I can't nginx mobile guide for drupal. I hope to this discussion can come out some possible solutions to use nginx and write guide to setup mobile site.

Basic goal is using nginx automatic redirection mobile & desktop user to respective theme/domain with their device accessing with. Next is having a BLOCK selection for Mobile theme to switch to desktop theme display.

Option 1. Mobile site using subdomain
http://drupal.org/node/1214890 using apache to rewrite
Need to convert it for nginx rewrite

Option 2. Mobile site using theme
http://drupal.org/node/629520#comment-3941580 using boost to cache mobile theme using apache rewrite.
I hope to have a discussion on how to create a light weight nginx redirect mobile and desktop

Reference on creating mobile site
http://drupal.org/node/459686
http://drupal.org/node/629520
http://drupal.org/node/1214890
http://cruncht.com/419/mobile-drupal-site-setup/

Comments

I don't see any reason

perusio's picture

compelling you to use Apache. Whatever Apache does, Nginx does it better, safer and with much less resources. If you're matching User-Agent headers then you can use map to define a variable that in the server context diverts the request to the appropriate site.
Any HTTP header can be used. Example:

if ($mobile_device) {
   return 302 http://m.site.com$request_uri;
}

As always, I prefer simple

omega8cc's picture

As always, I prefer simple solutions - if possible. You can review our progress on this feature: "Support separate Redis/Memcached, Boost and Speed Booster caches for various mobile devices" - http://drupal.org/node/1284002

There is a code/configuration already committed to the BOA head, however it is only the part related to some clear, easy to follow logic for caching per device group, while all the rest needs to be done on the Drupal side, as mentioned in the Barracuda issue.

You can made

perusio's picture

the config simpler, cleaner and snapier using map and relying on a single variable instead of percolating the logic on the vhost config.

map $http_user_agent $mobile_device {
      default 0;
      ~(?i)Nokia|BlackBerry.+MIDP|240x|320x|Palm|NetFront|Symbian|SonyEricsson  mobile-other;
     # if using a recent version 1.0.5 I believe (check the changelog) you can use ~* instead of the Perlism ~(?i).
     # etc...
}

Then on the vhost config do:

if ($mobile_device) { # A single if that is parametrized
   add_header X-Mobile-Device $mobile-device;
}

Note that if you do the redirect anything that happens later that the rewrite phase will be void. Meaning that using filters like add_header wont work unless you add it on the location that provides the content handler.

Just a note to not confuse

omega8cc's picture

Just a note to not confuse anyone - my config was wrong anyway, because add_header can't be used inside if {}. The map method works fine with other parts of my config.

Yes indeed

perusio's picture

add_header can only be used in http, server or location contexts.

You can get the same effect by using an error_page directive. Like this:

location / {
    (...)

     error_page 418 = @mobile;

    if ($mobile_device) {
       return 418;
    }
}

location @mobile {
   add_header X-Mobile-Device $mobile_device;
   # content handler needed for non static content
}

Not that not only that but is bad style, but also an improper use of if to any other thing that is not a rewrite with last or just do a return (or return with redirect). Easier still is just to define a default for non mobile and forego the if altogether. Like this:
map $http_user_agent $mobile_device {
    default non-mobile;
    ~*Nokia|BlackBerry.+MIDP|240x|320x|Palm|NetFront|Symbian|SonyEricsson mobile-other;
    # other patterns for User-Agent headers
}   

location / {
  add_header X-Mobile-Device $mobile_device;
  location = /index.php {
        # usual fastcgi stuff or proxy pass
        fastcgi_param PHP_VALUE "mobile_device_id=$mobile_device";
   }
}

Now you get a PHP variable $mobile_device_id to play with in Drupal and use it to branch into any device specific code. Many ways to catch a fly :)

Nice idea!

omega8cc's picture

Thanks perusio! :)

Oh, and one more thing I just

omega8cc's picture

Oh, and one more thing I just realized:

fastcgi_param PHP_VALUE "anything=$anything";

works only with php-fpm 5.3 (and of course breaks 5.2 when used).

Yes

perusio's picture

but if you're on Debian you can use the dotdeb repo and get 5.3.8 on Squeeze. Unless you have a very badly behaving module with PHP 5.3 you should upgrade.

I know. Unfortunately I can't

omega8cc's picture

I know. Unfortunately I can't switch to PHP 5.3 yet because too many Drupal 6 based distributions we host and support are using contrib modules not yet compatible with PHP 5.3.

Then

perusio's picture

you have to rely on a header to communicate something upstream :( Unless you're going to create a module that implements hook_menu and plays around with the query string. For example:

index.php?q=mobile&orig_uri=$uri&$args;

You'll have to parse the request URI and extract the appropriate information for handling that request. You'll use a try_files directive with that.

You can use 'fastcgi_param'

perusio's picture

it will show up on the

<?php
$_SERVER
;
?>
superglobal.

Here's an example:

fastcgi_param NO_APC 'TRUE';

Now if you check your phpinfo page inside the reports menu, you'll see a variable:
<?php
$_SERVER
['NO_APC'] // value is 'TRUE'
?>

Is not as clean as PHP_VALUE or PHP_ADMIN_VALUE, but it works!

@perusio

omega8cc's picture

Indeed, with map and a single if it does look more elegant and should be faster, thanks!

Note that in my code I don't use any redirects for mobile and the header is added only for debugging, as it is not used anywhere.

I think it is better to leave it for the app (Drupal) logic, or more precisely, for the dynamic theme logic, while on the server side just guarantee it will use device-specific caches.

Of course when you can hardcode some custom solution with rewrites, cookies etc on the web server (vhost) level, then it will be faster, but it will still require strict integration with the Drupal backend. It is not a good idea in the multisite (and multi-platform) environment like Aegir, but it can work on standalone installs, for sure.

Not wanting to start a

perusio's picture

flamewar. The fact is I never used much of drupal multisite facilities. I recall the discussion that ocurred at the Drupalcon London Pantheon BoF. I kind of agree that it gives you some gains in terms of reusing code, but there's the flip side of that. When you want to do more advanced stuff you're now many more levels deep in terms complexity. Because there's a lot of baggage that comes with multisite support.

I think that in the future multisite support will probably be dropped of Drupal. It's kinda of a throwback to the lean years of shared hosting and on a dime web hosting. Of course there are companies like omega8cc providing high-performance setups based on multisite. But I think that, like your reply above shows, using multisite comes with a price. It all boils down if you're willing to pay it.

BTW, if you weren't in that BoF here's something that should be of interest to us all Drupal on Nginx fans: Pantheon V2 is going to drop Apache and mod_php and it will be based on Nginx + php-fpm so I've been told.

@perusio

omega8cc's picture

I think you are not aware of Drupal multisite capabilities if you didn't use it with Aegir yet. The fact is I would never even start to considering Drupal for anything if it couldn't provide multisite out of the box. I would stay with the power and beauty of Textpattern instead :)

BTW, that is shockingly good news if the rumors about Pantheon V2 and Nginx + php-fpm will be true! They will ruin my not-yet-published comparison table :P

I got it

perusio's picture

from the horse's mouth. I arrived late at that BoF. I asked the question right at the end innocently about which stack were they using. I ended up talking with David Strauss for a few minutes after the BoF. And I've been told that you'll be able, for a fee, e.g., to tune the number of worker processes. Basically everything will be included in the package unless you try to tweak the config regarding directives that have direct hardware impact like the number of workers. That will cost you extra.

I think that a comparison of cloud solutions is due. I have yet to redeem my free Acquia DevCloud offer and see how it performs. BTW, if any of you can read spanish/portuguese/insert other neo-latin language than check out the web stack demolition derby ;) planned for friday October 31 right here.

As for Pantheon V2, it's slated for launching at BADcamp 2011, i.e., late October.

I use symlinks for reusing

brianmercer's picture

I use symlinks for reusing code and never use drupal multisite. I haven't found any reason to.

Using symlinks makes it easy to keep all your sites in separate trees for rsync, tmp dir, and backup purposes, makes it easy to do site.com/files instead of site.com/sites/site.com/files, makes it easy to do separate robots.txt and favicon.ico without code, avoids issues like site1.com/sites/site2.com/files/file.img, lets you switch sites between platforms individually and a host of other things.

Here's my skeleton structure:

skeleton6/
|-- backups
|-- cache
|-- current -> /usr/local/drupal/drupal6
|-- drupal
|   |-- cron.php -> ../current/cron.php
|   |-- files -> ../public/files/
|   |-- includes -> ../current/includes/
|   |-- index.php -> ../current/index.php
|   |-- install.php -> ../current/install.php
|   |-- misc -> ../current/misc
|   |-- modules -> ../current/modules/
|   |-- profiles -> ../current/profiles/
|   |-- robots.txt -> ../current/robots.txt
|   |-- sites
|   |   |-- all
|   |   |   |-- libraries -> /usr/local/drupal/libraries/
|   |   |   |-- modules -> /usr/local/drupal/modules6
|   |   |   `-- themes -> /usr/local/drupal/themes6
|   |   `-- default
|   |       |-- default.settings.php -> ../../../current/sites/default/default.settings.php
|   |       |-- files -> ../../../public/sites/default/files/
|   |       |-- modules
|   |       |-- settings.php
|   |       `-- themes
|   |-- themes -> ../current/themes/
|   |-- update.php -> ../current/update.php
|   `-- xmlrpc.php -> ../current/xmlrpc.php
|-- private
|   `-- files
|-- public
|   |-- files
|   |-- misc -> ../current/misc/
|   |-- modules -> ../current/modules/
|   |-- sites
|   |   |-- all
|   |   |   |-- libraries -> /usr/local/drupal/libraries/
|   |   |   |-- modules -> /usr/local/drupal/modules6
|   |   |   `-- themes -> /usr/local/drupal/themes6
|   |   `-- default
|   |       |-- files
|   |       |-- modules -> ../../../drupal/sites/default/modules/
|   |       `-- themes -> ../../../drupal/sites/default/themes/
|   `-- themes -> ../current/themes/
`-- tmp

When I want a new site I do "cp -pr /usr/local/drupal/skeleton6 /var/www/www.newsite.com".

Here's ls /usr/local/drupal:

drwxr-x---  7 root www-data 4.0K May  4 17:20 backup
-rwxr-x---  1 root www-data 1.1K Jan 17  2011 cron.sh
drwxr-x---  9 root www-data 4.0K Jan  5  2011 drupal-7.8
lrwxrwxrwx  1 root root       18 Jul  3 19:09 drupal6 -> pressflow-6.22.104
lrwxrwxrwx  1 root root       11 Jan  8  2011 drupal7 -> drupal-7.8/
drwxr-x--- 13 root www-data 4.0K Jun 15 20:38 libraries
drwxr-x---  6 root www-data 4.0K May 16 15:44 modules6
drwxr-x---  4 root www-data 4.0K Feb  6  2011 modules7
drwxr-x---  2 root www-data 4.0K Jun 19 21:46 patch6
drwxr-x---  2 root www-data 4.0K Oct  5  2010 patch7
drwxr-x---  9 root www-data 4.0K Jun 15 21:02 pressflow-6.22.102
drwxr-x---  9 root www-data 4.0K Jul  3 19:16 pressflow-6.22.104
drwxr-x---  8 root www-data 4.0K Jun 27 09:28 skeleton6
drwxr-x---  6 root www-data 4.0K Jan  8  2011 skeleton7
drwxr-x---  8 root www-data 4.0K May  4 17:22 themes6
drwxr-x---  2 root www-data 4.0K Oct  5  2010 themes7
drwxr-x---  2 root www-data 4.0K Jun 21  2010 tmp

Multiple layers of symlinks let me migrate an individual site to a new platform without taking the others with it. I can switch the "current" symlink to an updated core version individually, or do all sites at once by changing the drupal6 symlink.

All code is root writable but only readable by www-data and located in the proper LHS location for code.

The public html root is skeleton6/public. Cache files, backups, tmp files and private files are not in the public html root, nor is the settings.php file. You can direct backup_migrate to /backups, boost to /cache and temporary files to /tmp and you won't have any collisions, they're outside the web root, and they're portable since everything is under one tree. Drupal multisite lets you put all those items for each site in one dir under /sites/ but that means they're all in the public html root.

I've been very happy with this layout ever since I read http://justinhileman.info/article/a-more-secure-drupal-multisite-install/ and I've never found any reason to go back to using drupal's multisite feature. Justin uses dummy php files for cd and include, but they're unnecessary with the current nginx setup which specifies the code directory like this:

  location = /index.php {
    include /etc/nginx/fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $site_dir/drupal/index.php;
    fastcgi_pass php;
  }

I have used symlinks-based

omega8cc's picture

I have used symlinks-based "multisites" for years, both for Drupal and Textpattern. However, after switching to Aegir I could start using built-in Drupal multisites properly.

Drupal multisites are not about using the sites/do.main space for code. Absolutely no. The code is shared between sites in the same platform (or even between platforms) and lives in profiles/ or sites/all space.

Also, keep in mind that there is no such thing like safe "migration" if you don't use Aegir, because without Aegir you don't have any automated and instant rollback (both for code and database) in place.

To better understand the real multisites concept in Drupal and Aegir, I highly recommend this short intro with embedded presentation and links: http://omega8.cc/managing-your-code-in-the-aegir-style-110.

[EDIT] We still use symlinks for one thing in Aegir (optionally) - to separate big files directories.

Thanks for that link, I do

brianmercer's picture

Thanks for that link, I do envy some of those aegir features, but many of aegir's features are being abstracted out to drush so that you can take advantage of them without the aegir system.

It always worked that way and

omega8cc's picture

It always worked that way and it is one of Aegir project goals to get as much code as possible contributed upstream to Drush. But Drush doesn't handle safe rollbacks for sites migrations between platforms the way Aegir does.

Aegir is the tool which makes managing many Drupal sites really easy. Cloning or renaming the site takes 2 clicks. Migrating the site takes 2 clicks. Migrating all sites in a one batch between platforms: still 2 clicks. Plus safe, automated, instant rollback takes 0 clicks.

I agree that this

perusio's picture

is a much better approach. It opens up all the possibilities of using stuff like the above vhost tweaking for serving different devices. No need to create any PHP code. You just treat each site as if were a completely different site and let the file system handle the code sharing. You're a bunch of layers down and I venture that it will be faster since we won't be hitting any PHP code to do the site separation.

There is no PHP or DB

omega8cc's picture

There is no PHP or DB overhead when you use multisite. It is vhost in the web server what points to the correct platform and Drupal does the rest, as usual. There is no performance related difference between sites hosted in sites/default and sites/do.main. Noticed that the directory name is sites and not site? I don't think anyone will try to remove multisite support from Drupal, unless the node will be removed from core ;)

@omega8cc

perusio's picture

the upgrade/rollback feature was discussed at the above mentioned Pantheon BoF. If I understood correctly you're pegging all your sites together regarding the upgrade/downgrade. Let's say site X uses module Y that when you upgrade causes a problem. Now all your other sites will be rolled back due to this issue with one module in one site only. Is this correct?

I confess that I mostly ignorant regarding Aegir :(

Oh no, it doesn't work that

omega8cc's picture

Oh no, it doesn't work that way! :)

  1. You can migrate sites between platforms, one at the time.
  2. You can clone the site first to test migration - no need to risk your live site broken in the process.
  3. Even if you will choose batch migration, all sites will be migrated separately and automatically, so if any of them fails to migrate, Aegir will instantly rollback the migration task - for this one site only - and proceed with other sites normally.

For mobile sites, consider

brianmercer's picture

For mobile sites, consider using responsive web design if its feasible. Having css display the same data differently based on screen size is very elegant. http://coding.smashingmagazine.com/2011/01/12/guidelines-for-responsive-...

Also, consider using panels + mobile tools. Mobile Tools adds user agent detection to panels. This lets you use the same database and install for both mobile and desktop sites but allows you to serve completely different pages for mobile users. You can do that with a mobile subdomain or without.

nginx caching should work fine if you use the mobile subdomain. You'd have a different server{} for m.mysite.com and www.mysite.com and each would have its own nginx cache but then they'd both go to the exact same drupal installation. Panels would serve the appropriate page based on user agent and nginx would cache each site separately.

You might need some cookie checking at the nginx level if you want the feature whereby a user on a mobile device can opt to see the full site instead of the mobile site. Mobile tools adds the cookie, and it shouldn't be too difficult to add a cookie check in the mobile server{} that redirects the client to the desktop server{}.

I agree, responsive web

omega8cc's picture

I agree, responsive web design is probably the best approach.

Note that you can use separate caches in Nginx using dynamically set variable (via map) as a part of your fastcgi_cache_key (and/or as a name for separate Boost directory), so it will work without using separate subdomains. Examples:

http://drupalcode.org/sandbox/omega8cc/1074910.git/blob/ad3f785:/http/ng...
http://drupalcode.org/project/octopus.git/blob/HEAD:/aegir/conf/nginx_oc...
http://drupalcode.org/project/barracuda.git/blob/HEAD:/aegir/conf/global...

Digging deeper into perusio's

Mojah's picture

Digging deeper into perusio's posts and omega8cc's configurations, finally things began to clear up and I was able to get mobile and desktop caching to work the way we want it to for our distro. I'll briefly describe our problem and then solution to get what we wanted.

Requirement: Serve a multiple device, mobile friendly and desktop versions of websites running on our custom platform to anonymous visitors from database and static file caches.

Problem: Anonymous users would see either the mobile version or desktop version of the page regardless of what device they were accessing the site from.

We're running Aegir and a custom Drupal platform on Nginx, with Boost static file caching turned on. The platform offers a desktop version and mobile version of the site on the same domain, that is we don't use a sub-domain like m.example.com for our mobile sites. We noticed that with either Drupal's default db cache or Boost, we had the above mentioned issue.

First here's server level user agent detection. This set's a $device variable which is used in the Nginx location directive to see if a file exists on a local path, serving the file if it exists, instead of querying the database to build the page.

/**
* At this stage we only want to deliver a standard usable cross
* device  experience to all anon mobile users based on user agent *
* string matches. Here's the part we used from omega8cc's above
* linked config
/

map $http_user_agent $device {
  default normal;
~
Nokia|BlackBerry.+MIDP|240x|320x|Palm|NetFront|Symbian|SonyEricsson mobile;
~iPhone|iPod|Android|BlackBerry.+AppleWebKit mobile;
~
iPad|Tablet mobile;
}

Here's a look at how the location directive uses the $device variable in the try_files function.
try_files /cache/$device/$host${uri}_$args.html @drupal;

In our use case, the $device variable can only be set to "normal" or "mobile".

Then we added this to Aegir's global.inc located in .../aegir/config/includes/, which detects the device again at the php level and sets the custom Boost file cache paths.

  if (isset($_SERVER['HTTP_USER_AGENT'])) {
    if (preg_match("/(?:Nokia|BlackBerry.+MIDP|240x|320x|Palm|NetFront|Symbian|SonyEricsson)/i", $_SERVER['HTTP_USER_AGENT'])) {
      $this_device = 'mobile';
    }
    elseif (preg_match("/(?:iPhone|iPod|Android|BlackBerry.+AppleWebKit)/i", $_SERVER['HTTP_USER_AGENT'])) {
      $this_device = 'mobile';
    }
    elseif (preg_match("/(?:iPad|Tablet)/i", $_SERVER['HTTP_USER_AGENT'])) {
      $this_device = 'mobile';
    }
    else {
      $this_device = 'normal';
    }
  }
  else {
    $this_device = 'normal';
  }
  $conf['boost_normal_dir'] = $this_device;
  $conf['boost_gzip_dir'] = $this_device;

Sometimes, Boost is turned off and Drupal's default cache is turned on. Seeing that the majority of websites on our platform only have a small percentage of mobile users (~2-3% out of 150-3K unique users) we felt at this stage it was safe enough to just bypass the database cache when a mobile device was detected, serving up fresh content. Later on we could probably write a customised $cid value to Drupals cache_page table for mobile devices.

Besides the above, there are 2 immediate things I need to figure out yet.

1 - How to provide a link that allows the user to switch between mobile and desktop versions. Presently we make that decision for the user without giving them an option to change.

2 - From the comments, I'm pretty sure we can set a global php value from within the Nginx main config file. I read something about using a fastcgi_param to set this value and it would be available in the _SERVER variable

I'll update here when I have a solution. Suggestions or corrections anyone?

Thank you for your breakdown

Tim Jones Toronto's picture

Thank you for your breakdown and nice to see how things are going with your project. I am getting my head around some of this myself so appreciate your experience.

Would it possible to determine the default state in PHP and create a boolean type toggle session cookie (e.g. mobile == true) using a link. Then determine action based on:

if ($http_cookie ... ) {
then..
}

in the NGINX server logic config script?

On a side note, there is also http://detectmobilebrowsers.com/ which I just found for NGINX too.

Sorry if i am off-track here :)

Tim

Yes

perusio's picture
  1. Your map should try to filter for desktop and bots, since they have a smaller fauna
    than mobiles. See here.

  2. The easiest way is to do:

    fastcgi_param MOBILE_DEVICE $device;

    In your script recover the value via:
    <?php
    $_SERVER
    ['MOBILE_DEVICE']
    ?>

Of course if you want do differentiate between the several mobile devices then you need something more fine grained.

Yeah! It's working. Thank you

Mojah's picture

Yeah! It's working. Thank you so much!

This took a while to figure out. Nginx newbie here. First I figured out how to access $device from php as you suggested.

@Tim Jones Toronto, here's what I added:

fastcgi_param  MOBILE_DEVICE      $device;

to .../aegir/config/includes/fastcgi_params.conf

and was able to access the $_SERVER['MOBILE_DEVICE'] from php.

Once I got this working, I decided to switch the logic as @perusio suggested to test for desktop/bots, instead of schlepping through the growing number of mobile devices. I did come across the this method of mobile device detection in Nginx before, but was not able to figure out how to use it until now.

After several experiments with the configuration files on my local this is what I ended up with...

In .../aegir/config/includes/fastcgi_params.conf

fastcgi_param  USER_DEVICE      $device;

In .../aegir/config/aegir.conf

map $http_user_agent $device {
    default mobile;
    ~linux.android|windows\s+(?:ce|phone) mobile; # exceptions to the rule
    ~spider|crawl|slurp|bot normal; # bots
    ~
windows|linux|os\s+x\s*[\d._]+|solaris|bsd normal; # OSes
}

In our global.inc for settings.php (../aegir/config/includes/global.inc)

  $conf['boost_normal_dir'] = $_SERVER['USER_DEVICE'];
  $conf['boost_gzip_dir'] = $_SERVER['USER_DEVICE'];

From testing on various devices/user agents, the above set-up seems to be working exactly how we want it. Later on if we need to deliver customized layouts for specific devices, we can always adjust the above.

No.2 above is probably not

Mojah's picture

No.2 above is probably not the best for multisite environment as @omega8cc suggested here so it appears that it's better to test for device at the Drupal/php level.

Nginx

Group organizers

Group notifications

This group offers an RSS feed. Or subscribe to these personalized, sitewide feeds: