7.2.2.2.7. Contenido dinámico

nginx no ejecuta directamente código para generar el contenido dinámico, sino que actúa de proxy hacia el intérprete adecuado. Esta es una gran diferencia respecto a apache que suele tener módulos para la ejecución de los distintos lenguajes.

7.2.2.2.7.1. PHP

7.2.2.2.7.1.1. Instalación

Para dar soporte a aplicaciones escritas en PHP y que hagan uso de bases de datos MySQL[1] combinación muy frecuente es necesario como mínimo lo siguiente[2]:

# apt-get install php-{fpm,mysql} mariadb-{server,client}

Esta es una instalación muy mínima y cualquier aplicación PHP medianamente seria requerirá algún paquete más. Como alternativa, podemos instalar lo siguiente:

# apt-get install php{,-mysql} libapache2-mod-php7.3- mariadb-{server,client}

Nota

En este caso, php instala librerías muy comunes y que muy probablemente las necesite alguna aplicación que instalemos después. Sin embargo, php prefiere como dependencia apache, así que evitamos su instalación.

Aunque no usemos este segundo método de instalación, sino el primero, el truco de evitar explícitamente la dependencia de apache es válido cuando, al instalar una aplicación web desde repositorios (p.e. roundcube), debian nos pretenda instalar también este servidor.

7.2.2.2.7.1.2. Configuración del sitio

Resueltas las dependencias, hay que configurar nginx. Lo más sencillo es crear el siguiente archivo[3]:

# cat /etc/nginx/conf.d/php.conf
upstream php {
   server unix:/var/run/php/php-fpm.sock;
}

y habilitar el uso del intérprete para los scripts de PHP, por ejemplo, así:

server {
   listen 80;

   server_name _;

   root /srv/www;
   try_files $uri $uri/ =404;
   index  index.php;

   location ~ \.php$ {
      include snippets/fastcgi-php.conf;
      fastcgi_pass php;
   }
}

Nota

Tenga en cuenta que esto ejecuta como PHP cualquier archivo cuyo extensión sea .php. Si la aplicación permite subir archivos, esto provocará un agujero de seguridad, ya que podría permitir subir un script al servidor y ejecutarlo luego. Sería conveniente excluir el directorio de subidas para evitarlo. Suponiendo que este se llame /wp-content/uploads/:

location ~ ^((?!/wp-content/uploads/).)*\.php$ {
   include snippets/fastcgi-php.conf;
   fastcgi_pass php;
}

o bien dejarlo como estaba en un principio y añadir:

location ^~ /wp-content/uploads/ {
}

que evitará que aplicar el bloque de la localización anterior cuando la ruta esté dentro de /wp-content/uploads.

7.2.2.2.7.1.3. Comprobación

La prueba más sencilla para comprobar si se interpreta PHP es crear un archivo index.php:

# echo '<?php phpinfo(); ?>' > /srv/www/index.php

que devuelve la actual configuración del intérprete PHP. Si, además, queremos probar el acceso a la base de datos podemos descargar esta prueba muy simple con un script que crea una tabla HTML a partir de los datos almacenados en una base de datos. Basta con colocar el script en una localización en que se ejecute y volcar el guión SQL en la base de datos:

# mysql < guion.sql

7.2.2.2.7.1.4. Optimización por cacheo

El servicio de páginas dinámicas es muy costoso en la medida en que su petición exige la generación al vuelo del código HTML. Por ello, una muy buena optimización es que el servidor cachee la página generada, de suerte que posteriores peticiones no generen la página, sino que entreguen la página ya generada. Es obvio que si el contenido es dinámico se debe a que cambia y que, en consecuencia, cachear es muy peligroso en la medida en que, si la configuración no es la adecuada, estaremos remitiendo al cliente contenido obsoleto.

Advertencia

Hay que estudiar muy concienzudamente cómo, cuándo y durante cuánto tiempo se cacheará a fin de que los clientes no reciban contenido cacheado obsoleto.

Antes de empezar, no obstante, es muy útil crear una página dinámica por la que conozcamos de un vistazo si la página, de petición a petición, cambia o no:

<?php
   header('Content-type: text/plain; charset=utf-8');
   echo 'Página generada en el momento '.time();
?>

que podemos colocar como archivo index.php y nos servirá para hacer pruebas. Es obvio que, si no cacheamos, cada vez que pidamos la página obtendremos una página que devuelve un tiempo UNIX distinto.

Para cachear el PHP hay que crear primero un directorio de caché:

# mkdir -m700 /var/cache/nginx/wp-cache
# chown www-data /var/cache/nginx/wp-cache

y, luego, una configuración de este estilo para nginx (remarcamos los cambios respecto a la configuración que no cachea):

fastcgi_cache_path /var/cache/nginx/wp-cache
                   levels=1:2
                   keys_zone=wp-cache:100m
                   inactive=7m;

server {
   listen 80;

   server_name _;

   root /srv/www;
   try_files $uri $uri/ =404;
   index  index.php;

   location ~ \.php$ {
      include snippets/fastcgi-php.conf;
      fastcgi_pass php;

      fastcgi_cache wp-cache;
      fastcgi_no_cache $http_authorization;
      fastcgi_cache_valid 1m;
      fastcgi_cache_bypass $http_pragma;  # Evita la cache con Ctrl+F5 (Pragma: no-cache)
      fastcgi_cache_key "$scheme$host$request_uri";
      fastcgi_cache_use_stale updating error timeout invalid_header http_500;
      fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
      add_header X-RunCloud-Cache $upstream_cache_status;
   }
}

Esta configuración supone:

  • Mediante fastcgi_cache_path se define como directorio de caché, el directorio /var/cache/nginx/wp-cache antes creado. A esta caché dedicarenos 100MB y nos referiremos a ella como wp-cache, según se determina con keys_zone. Además, determinamos que, a los 7 minutos de haber cacheado una página, ésta desaparezca de la caché.

  • En la location para PHP añadimos que se cachee usando la caché anterior y, además:

    • fastcgi_no_cache permite definir cuándo no se quiere cachear en absoluto. No se cacheará cuando la variable tenga algún valor y este no sea 0. Pueden añadirse varias, en cuyo caso bastará con que alguna tenga un valor distinto a 0.

    • Fijamos que los contenidos cacheados tienen una validez de 1 minuto. Como la línea es esta:

      fastcgi_cache_valid 1m;
      

      y no se ha expresado código de respuesta alguno, sólo se cachean durante un minuto las respuestas correctas (código 200) y las redirecciones (códigos 301 y 302). Cualquier otro código, no es cacheado. Pueden, por supuesto, cachearse otros códigos, incluso todos usando la palabra any:

      fastcgi_cache_valid any 1m;
      

      o varios con distinto tiempo usando varias veces la directiva:

      fastcgi_cache_valid 200 301 302 307 1m;
      fastcgi_cache_valid 404 30s;
      

      Nota

      El tiempo apropiado, por supuesto, dependerá de cuál sea la la aplicación web y cuál la situación que queremos resolver.

    • Evitamos los contenidos cacheados (fastcgi_cache_bypass) cuando el cliente envía una cabecera:

      Pragma: no-cache
      

      lo cual ocurre cuando desde un navegador se refresca la página pulsando Ctrl+F5. Es importante notar que, al puentear la caché, se vuelve a generar el contenido, lo que supone no solamente que obtenegamos el nuevo contenido, sino que se renueve la caché.

    • Hacemos que se identifique cada recurso cacheado con la cadena «$scheme$host$request_uri». Esto significa que si una nueva petición construye una cadena exactamente igual a la que construyó una petición anterior, nginx devolverá el cacheo de esta petición anterior.

    • Con fastcgi_cache_use_stale definimos bajo qué circunstancias es permisible devolver una respuesta obsoleta cacheada. En el ejemplo, si al hacer una petición se obtiene un error 500, pero la caché conservaba una respuesta obsoleta (recordemos que las respuestas caducan al minuto, pero que no se borrar hasta pasados 7), entonces en vez de devolver tal error, nginx devolverá la respuesta obsoleta. También es posible obtener una respuesta obsoleta, si se recibe una petición durante el tiempo de actualización de la caché (debido al parámetro updating incluido).

    • No atendemos campos en la cabecera de respuesta (Cache-Control, Expires, Set-Cookie) que podrían alterar nuestra política de cacheo. De hecho, es bastante común que las aplicaciones web incluyan este tipo de cabeceras para evitar que proxies intermedios cacheen contenido que es dinámico.

    • Por último, incluimos añadimos una cabecera que informa al cliente de si el contenido está cacheado o no: un valor de MISS significa que el contenido se generó para nuestra solicitud y uno de HIT que se obtuvo de la caché.

Podemos hacer pruebas con un navegador, pero si disponemos de linux en nuestra máquina cliente, es bastante más cómodo usar wget:

  1. Generamos contenido por primera vez (y, de paso, miramos las cabeceras):

    $ wget -qSO - http://www.example.net
      HTTP/1.1 200 OK
      Server: nginx/1.10.3
      Date: Thu, 08 Nov 2018 15:41:51 GMT
      Content-Type: text/plain; charset=utf-8
      Transfer-Encoding: chunked
      Connection: keep-alive
      X-RunCloud-Cache: MISS
    Página generada en el momento 1541691711
    
  2. Volvemos a pedir la página, sin esperar a que pase el minuto:

    $ wget -qSO - http://www.example.net
      HTTP/1.1 200 OK
      Server: nginx/1.10.3
      Date: Thu, 08 Nov 2018 15:42:30 GMT
      Content-Type: text/plain; charset=utf-8
      Transfer-Encoding: chunked
      Connection: keep-alive
      X-RunCloud-Cache: HIT
    Página generada en el momento 1541691711
    
  3. Pedimos de nuevo el contenido (deberá regenerarse):

    $ wget -qSO - http://www.example.net
      HTTP/1.1 200 OK
      Server: nginx/1.10.3
      Date: Thu, 08 Nov 2018 15:45:56 GMT
      Content-Type: text/plain; charset=utf-8
      Transfer-Encoding: chunked
      Connection: keep-alive
      X-RunCloud-Cache: EXPIRED
    Página generada en el momento 1541691984
    
  4. Emulamos el refresco con Ctrl+F5:

    $ wget -qSO - --header "Pragma: no-cache" http://www.example.net
      HTTP/1.1 200 OK
      Server: nginx/1.10.3
      Date: Thu, 08 Nov 2018 15:46:21 GMT
      Content-Type: text/plain; charset=utf-8
      Transfer-Encoding: chunked
      Connection: keep-alive
      X-RunCloud-Cache: BYPASS
    Página generada en el momento 1541692253
    

Purgando la caché

nginx dispone directivas para purgar de forma manual la caché, pero forman parte de la versión de pago. debian, sin embargo, permite la instalación del módulo ngx_cache_purge que habilita precisamente eso:

# apt install libnginx-mod-http-cache-purge

Las directivas de purga (fastcgi_cache_purge en nuestro caso no pueden usarse dentro de un directiva if), por lo que una argucia para poder purgar sería la siguiente configuración:

fastcgi_cache_path /var/cache/nginx/wp-cache
                   levels=1:2
                   keys_zone=wp-cache:100m
                   inactive=7m;

server {
   listen 80;

   server_name _;

   root /srv/www;
   try_files $uri $uri/ =404;
   index  index.php;

   if ($request_method = "PURGE") {
      rewrite ^ /purge$request_uri last;
   }

   location ~ /purge(/.*)$ {
      internal;
      allow 127.0.0.0/8;
      deny all;

      fastcgi_cache_purge wp-cache $request_uri;
   }

   location ~ \.php$ {
      include snippets/fastcgi-php.conf;
      fastcgi_pass php;

      fastcgi_cache wp-cache;
      fastcgi_no_cache $http_authorization;
      fastcgi_cache_valid 1m;
      fastcgi_cache_bypass $http_pragma;  # Evita la cache con Ctrl+F5 (Pragma: no-cache)
      fastcgi_cache_key $request_uri;
      fastcgi_cache_use_stale updating error timeout invalid_header http_500;
      fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
      add_header X-RunCloud-Cache $upstream_cache_status;
   }

}

Con esta configuración podremos purgar de la caché usando el método PURGE, pero sólo desde el propio servidor. Por tanto, si obtenemos así, la página:

$ wget -qSO - http://www.example.net

podremos purgar desde el propio servidor de este modo:

$ wget -qSO - --method=PURGE "http://localhost"

7.2.2.2.7.2. Otros lenguajes

Notas al pie