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 conkeys_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:
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
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
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
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