Programación

Configurar VPS desde cero VI: servidor web nginx y cache varnish ssl

Configurar VPS desde cero VI: servidor web nginx y cache varnish ssl
5 (100%) 1 voto[s]

Varnish ssl. Si alguna vez has realizado esta búsqueda en Google, estas de enhorabuena. En este articulo descubrirás como integrar el proxy de cache inverso Varnish junto con Nginx, respetando el acceso https e incluyendo otras optimizaciones para la mejora de la velocidad como el formato de imágenes webp.

En los artículos anteriores aprendimos como instalar el servidor web Nginx junto con la compresión brotli y el modulo pagespeed para optimizar su rendimiento.

En esta ocasión, iremos un paso mas allá, e incluiremos el cache proxy varnish para una respuesta más rápida del servidor a los clientes a la hora de cargar nuestra pagina web.

¿Porque utilizar Varnish?

El principal cometido de Varnish es el de cachear recursos y partes de nuestra pagina web según los criterios que definamos y servirlos de manera más rápida al no tener que realizar la llamada al servidor web Nginx para obtenerlos.

Como la velocidad de acceso de la memoria es mucho mas rápida que el acceso a disco, unido a que Varnish utiliza parte de la memoria del vps como cache; nos permite acelerar mucho el tiempo de carga de nuestra web.

Cambio de estructura del servidor web

Hasta ahora, el servidor web estaba funcionando de la siguiente manera:

  • Para las peticiones https: Nginx estaba escuchando el puerto 443
  • Para las peticiones http: Nginx escuchando el puerto 80, redireccionando cualquier petición a la url https

Como Varnish no soporta SSL/TLS, no puede escuchar las peticiones que nos llegan por https. Como a estas alturas no podemos renunciar al https, deberemos hacer un pequeño cambio en la estructura del servidor web:

  • Nginx escuchara el puerto 443 para las peticiones https. Nginx escuchara el puerto 443 y redireccionará a Varnish las peticiones en el puerto 6081, esperando su respuesta para devolverla al cliente.
  • Varnish escuchara las peticiones por el puerto 6081 y devolverá las que ya estén cacheadas. En caso de no ser así, realizara a Nginx la petición por el puerto 81
  • Nginx escuchara el puerto 81 y responderá las peticiones de Varnish, devolviendo el contenido solicitado.

De esta manera, Varnish cacheara la respuesta recibida por Nginx y la próxima vez que sea solicitada, la devolverá sin volver a realizar la petición a Nginx, ahorrando tiempo de respuesta y mejorando la velocidad de carga de la pagina web.

Instalación y configuración del proxy cache Varnish

Instalación de Varnish desde el repositorio

Comenzamos instalando el cache proxy Varnish. En la consola de nuestro vps, tecleamos:

sudo apt-get update
sudo apt-get install debian-archive-keyring

A continuación instalamos las herramientas necesarias para Varnish:

sudo apt-get install curl gnupg apt-transport-https

Y descargamos la clave gpg de firma para poder comprobarla del repositorio:

curl -L https://packagecloud.io/varnishcache/varnish62/gpgkey | sudo apt-key add -

Ahora viene el paso más importante. Lo primero, debemos saber qué distribución y que version tenemos en nuestro vps. En mi caso es una Debian/jessie, por lo que

deb https://packagecloud.io/varnishcache/varnish62/debian/ jessie main
deb-src https://packagecloud.io/varnishcache/varnish62/debian/ jessie main

Si no utilizas Debian jessie, deberas cambiar los datos en negrita por los correspondientes a tu distro. En caso de desconocer la distribución linux que tienes instalada, desde una ventana del vps, teclea:

lsb_release -a

Te mostrara algo similar a esto, donde especifica la distribución y la version que tienes actualmente instalada en tu vps (en mi caso debian/jessie):

No LSB modules are available.
Distributor ID:	Debian
Description:	Debian GNU/Linux 8.11 (jessie)
Release:	8.11
Codename:	jessie

Una vez añadido el repositorio correspondiente a nuestra distribución linux, deberemos actualizar la lista de paquetes instalables:

sudo apt-get update

Para, a continuación, instalar la version 6.2 de Varnish:

apt-get install varnish

Configuración de Varnish SSL

Lo primero que debemos hacer es modificar la configuración de Varnish para adaptarla al esquema explicado anteriormente.

El fichero de configuración de Varnish se encuentra /etc/default/varnish. Por defecto, el contenido de este fichero de configuración de Varnish es:

DAEMON_OPTS="-a *:80 \
             -T localhost:6082 \
             -f /etc/varnish/default.vcl \
             -S /etc/varnish/secret \
             -s malloc,256m"

Esta configuración indica que Varnish escuchará las peticiones que lleguen por el puerto 80. Evidentemente, deberemos modificarla para que escuche el puerto 6081, que es el puerto donde nos llegaran las peticiones de Nginx que estará escuchando el puerto 443 para las peticiones ssl.

Modificamos el puerto donde escuchara las peticiones Varnish, dejando el contenido del mismo así:

DAEMON_OPTS="-a localhost:6081 \
             -T localhost:6082 \
             -f /etc/varnish/default.vcl \
             -S /etc/varnish/secret \
             -s malloc,256m"

Ahora toca el turno de modificar el fichero de configuración que controla las reglas de cacheo de Varnish. Este fichero esta en /etc/varnish/default.vcl.

# Marker to tell the VCL compiler that this VCL has been adapted to the
# new 4.0 format.
vcl 4.0;
import std;

# Default backend definition. Set this to point to your content server.
backend default {
    .host = "127.0.0.1";
    .port = "81";
    .connect_timeout = 600s;
    .first_byte_timeout = 600s;
    .between_bytes_timeout = 600s;
    .max_connections = 800;
}

acl purge {
  "localhost";
  "127.0.0.1";
  "DIRECCION_IP_DE_MI_VPS";
}
# This function is used when a request is send by a HTTP client (Browser)
sub vcl_recv {
        # Normalize the header, remove the port (in case you're testing this on various TCP ports)
        set req.http.Host = regsub(req.http.Host, ":[0-9]+", "");

        # Allow purging from ACL
        if (req.method == "PURGE") {
                # If not allowed then a error 405 is returned
                if (!client.ip ~ purge) {
                        return(synth(405, "This IP is not allowed to send PURGE requests."));
                }
                # If allowed, do a cache_lookup -> vlc_hit() or vlc_miss()
                return (purge);
        }

        # Post requests will not be cached
        if (req.http.Authorization || req.method == "POST") {
                return (pass);
        }

        # --- WordPress specific configuration

        # Did not cache the RSS feed
        if (req.url ~ "/feed") {
                return (pass);
        }
        # Blitz hack
        if (req.url ~ "/mu-.*") {
                return (pass);
        }


        # Did not cache the admin and login pages
        if (req.url ~ "/wp-(login|admin)") {
                return (pass);
        }


        # Remove the "has_js" cookie
        set req.http.Cookie = regsuball(req.http.Cookie, "has_js=[^;]+(; )?", "");

        # Remove any Google Analytics based cookies
        set req.http.Cookie = regsuball(req.http.Cookie, "__utm.=[^;]+(; )?", "");

        # Remove the Quant Capital cookies (added by some plugin, all __qca)
        set req.http.Cookie = regsuball(req.http.Cookie, "__qc.=[^;]+(; )?", "");

        # Remove the wp-settings-1 cookie
        set req.http.Cookie = regsuball(req.http.Cookie, "wp-settings-1=[^;]+(; )?", "");

        # Remove the wp-settings-time-1 cookie
        set req.http.Cookie = regsuball(req.http.Cookie, "wp-settings-time-1=[^;]+(; )?", "");

        # Remove the wp test cookie
        set req.http.Cookie = regsuball(req.http.Cookie, "wordpress_test_cookie=[^;]+(; )?", "");

        # Are there cookies left with only spaces or that are empty?
        if (req.http.cookie ~ "^ *$") {
                    unset req.http.cookie;
                    unset req.http.Pragma;
        }

        # Cache the following files extensions
        if (req.url ~ "\.(css|js|png|gif|jp(e)?g|swf|ico)") {
                unset req.http.cookie;
        }

        # Normalize Accept-Encoding header and compression
        # https://www.varnish-cache.org/docs/3.0/tutorial/vary.html
        if (req.http.Accept-Encoding) {
                # Do no compress compressed files...
                if (req.url ~ "\.(jpg|png|gif|gz|tgz|bz2|tbz|mp3|ogg)$") {
                                unset req.http.Accept-Encoding;
                } elsif (req.http.Accept-Encoding ~ "gzip") {
                        set req.http.Accept-Encoding = "gzip";
                } elsif (req.http.Accept-Encoding ~ "deflate") {
                        set req.http.Accept-Encoding = "deflate";
                } elsif (req.http.Accept-Encoding ~ "br") {
                        set req.http.Accept-Encoding = "br";
                } else {
                        unset req.http.Accept-Encoding;
                }
        }

        # Check the cookies for wordpress-specific items
        if (req.http.Cookie ~ "wordpress_" || req.http.Cookie ~ "comment_") {
                return (pass);
        }
        if (!req.http.cookie) {
                unset req.http.cookie;
        }

        # --- End of WordPress specific configuration

        # Do not cache HTTP authentication and HTTP Cookie
        if (req.http.Authorization || req.http.Cookie) {
                # Not cacheable by default
                return (pass);
        }

        # Cache all others requests
        return (hash);
}

sub vcl_pipe {
        return (pipe);
}

sub vcl_pass {
        return (fetch);
}

# The data on which the hashing will take place
sub vcl_hash {
        hash_data(req.url);
        if (req.http.host) {
             hash_data(req.http.host);
        } else {
             hash_data(server.ip);
        }

        # If the client supports compression, keep that in a different cache
        if (req.http.Accept-Encoding) {
             hash_data(req.http.Accept-Encoding);
        }

        return (lookup);
}


# This function is used when a request is sent by our backend (Nginx server)
sub vcl_backend_response {
        # Remove some headers we never want to see
        unset beresp.http.Server;
        unset beresp.http.X-Powered-By;

        # For static content strip all backend cookies
        if (bereq.url ~ "\.(css|js|png|gif|jp(e?)g)|swf|ico") {
                unset beresp.http.cookie;
        }

        # Only allow cookies to be set if we're in admin area
        if (beresp.http.Set-Cookie && bereq.url !~ "^/wp-(login|admin)") {
                unset beresp.http.Set-Cookie;
        }

        # don't cache response to posted requests or those with basic auth
        if ( bereq.method == "POST" || bereq.http.Authorization ) {
                set beresp.uncacheable = true;
                set beresp.ttl = 120s;
                return (deliver);
        }

        # don't cache search results
        if ( bereq.url ~ "\?s=" ){
                set beresp.uncacheable = true;
                set beresp.ttl = 120s;
                return (deliver);
        }

        # only cache status ok
        if ( beresp.status != 200 ) {
                set beresp.uncacheable = true;
                set beresp.ttl = 120s;
                return (deliver);
        }

        # A TTL of 24h
        set beresp.ttl = 24h;
        # Define the default grace period to serve cached content
        set beresp.grace = 30s;

        return (deliver);
}

# The routine when we deliver the HTTP request to the user
# Last chance to modify headers that are sent to the client
sub vcl_deliver {
        if (obj.hits > 0) {
                set resp.http.X-Cache = "cached";
        } else {
                set resp.http.x-Cache = "uncached";
        }

        # Remove some headers: PHP version
        unset resp.http.X-Powered-By;

        # Remove some headers: Apache version & OS
        unset resp.http.Server;

        # Remove some headers: Varnish
        unset resp.http.Via;
        unset resp.http.X-Varnish;

        return (deliver);
}

sub vcl_init {
        return (ok);
}

En este fichero debemos cambiar DIRECCION_IP_DE_MI_VPS por la dirección IP de nuestro vps. De esta manera podremos purgar la cache si es necesario desde consola o algún plugin de cache para wordpress.

Modificación de la configuración en Nginx

Modificaciones en el fichero de configuración de Nginx

Deberemos modificar el fichero de configuración de nuestro sitio en Nginx para cambiar los puertos donde estar escuchando y que así pueda comunicarse con Varnish.

Como en mi vps tengo instalado ispconfig como gestor en el puerto 8080, utilizare Nginx tanto en el puerto 443 para las peticiones ssl como en el 81 para procesar el contenido de las paginas.

En mi caso, he creados dos ficheros de configuración por cada dominio, aunque si lo prefieres se puede juntar todo en un fichero único:

  • /etc/nginx/sites-enabled/100-proxy.midominio.es: este es el fichero donde tengo las reglas de Nginx para gestionar el servidor que escucha el puerto 443 y atiende las peticiones https
  • /etc/nginx/sites-enabled/100-midominio.es: el fichero donde configuro el servidor Nginx para escuchar el puerto 81, recibir las peticiones de Varnish y procesar las peticiones de la web en caso de no estar cacheadas

Fichero de configuración de Nginx en puerto ssl 443 (https)

Este el contenido del fichero /etc/nginx/sites-enabled/100-proxy.midominio.es . En negrita resalto los datos que debemos cambiar para adaptarlo al dominio que deseamos configurar:

server {
  server_name www.midominio.es midominio.es;

  listen 443 ssl http2;

  access_log /var/log/ispconfig/httpd/midominio.es/varnish-proxy-access.log;
  error_log /var/log/ispconfig/httpd/midominio.es/varnish-proxy-error.log;

  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_certificate /etc/letsencrypt/live/www.midominio.es/fullchain.pem; # managed by Certbot
  ssl_certificate_key /etc/letsencrypt/live/www.midominio.es/privkey.pem; # managed by Certbot

  keepalive_timeout 300s;

  location / {
    #BYPASS VARNISH
    #proxy_pass http://127.0.0.1:81;
    #VARNISH ENABLED
    proxy_pass http://127.0.0.1:6081;

    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-Host $http_host;
    proxy_set_header X-Real-IP  $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-Port 443;
    proxy_set_header X-Secure on;

    proxy_redirect     off;
  }
}

En este fichero debemos modificar los datos resaltado en negrita por los utilizados por nuestra web:

  • server_name: nombre de dominio de nuestro sitio web
  • access_log: ruta completa del fichero de log de los acceso al sitio web
  • error_log: ruta completa del fichero de log de los errores del sitio web
  • ssl_certificate y ssl_certificate_key: ruta completa a los ficheros del certificado ssl de nuestro sitio web

Si algún día nos surge algún problema, podemos comentar la linea proxy_pass http://127.0.0.1:6081 y descomentar la linea proxy_pass http://127.0.0.1:81. De esta manera, nos saltaremos Varnish, por lo que podremos comprobar si el problema esta en la parte de Nginx o varnish.

Fichero de configuración de Nginx en el puerto 80 (http) y 81

Este el contenido del fichero /etc/nginx/sites-enabled/100-midominio.es.

server {
        listen *:80;

        server_name www.midominio.es midominio.es;

        access_log   /var/log/ispconfig/httpd/midominio.es/access-80.log combined;
        error_log    /var/log/ispconfig/httpd/midominio.es/error-80.log;

        rewrite ^ https://$http_host$request_uri? permanent;
}
server {
        listen *:81;

        server_name www.midominio.es midominio.es;

        root   /var/www/midominio.es/web/;

        error_log /var/log/ispconfig/httpd/midominio.es/error.log;
        access_log /var/log/ispconfig/httpd/midominio.es/access.log combined;

        index index.html index.htm index.php index.cgi index.pl index.xhtml;

        add_header Cache-Control "public, max-age=31536000, s-maxage=31536000";

        gzip on;
        gzip_vary on;
        gzip_proxied any;
        gzip_comp_level 6;
        gzip_buffers 16 8k;
        gzip_http_version 1.1;
        gzip_types image/svg+xml text/plain text/xml text/css text/javascript
                   application/xml application/xhtml+xml application/rss+xml application/javascript
                   application/x-javascript application/x-font-ttf application/vnd.ms-fontobject
                   font/opentype font/ttf font/eot font/otf;


        brotli              on;
        brotli_comp_level   6;
        brotli_min_length   256;
        # text/html is always compressed
        brotli_types application/javascript application/json application/xml image/svg+xml text/css
                     text/plain text/xml text/javascript application/xhtml+xml application/rss+xml
                     application/x-javascript application/x-font-ttf application/font-woff
                     application/vnd.ms-fontobject font/opentype font/ttf font/eot font/woff2
                     font/otf;

        # Brotli static
        brotli_static on;

        
        location ~ /\. {
                deny all;
        }

        location ^~ /.well-known/acme-challenge/ {
                access_log off;
                log_not_found off;
                root /usr/local/ispconfig/interface/acme/;
                autoindex off;
                index index.html;
                try_files $uri $uri/ =404;
        }

        location = /favicon.ico {
                log_not_found off;
                access_log off;
                expires max;
                add_header Cache-Control "public";
        }

        location = /robots.txt {
                allow all;
                log_not_found off;
                access_log off;
        }

        location ~ \.php$ {
                try_files /021b17524f532369c27847cd783443d0.htm @php;
        }

        location @php {
                try_files $uri =404;
                include /etc/nginx/fastcgi_params;
                fastcgi_pass 127.0.0.1:9010;
                fastcgi_index index.php;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                fastcgi_intercept_errors on;
        }

        location / {
                try_files $uri $uri/ /index.php?$args;
        }

        location ~* ^.+\.(jpeg|png|jpg) {
                add_header Cache-Control "public";
                add_header Vary "Accept";
                expires max;
                if ($http_accept !~* "webp"){
                        break;
                }
                try_files $uri$webp_extension $uri =404;
        }

        rewrite /wp-admin$ $scheme://$host$uri/ permanent;

}

Una vez realizados los cambios en los ficheros, podemos comprobar que son correctos y recargar la configuración de Nginx con el comando:

nginx -t && nginx -s reload

De esta manera evitamos que por un error en el fichero de configuración el servidor Nginx se detenga y nuestra web deje de estar operativa.

Modificación de a configuración de WordPress

Al incorporar Varnish, y para no tener problemas a la hora de acceder al panel de control de wordpress, debemos hacer una pequeña modificación en el fichero wp-config.php de nuestro sitio web.

Modificaciones en el fichero wp-config.php de WordPress

Si nuestra web utiliza wordpress, como es mi caso, para que el panel de control funcione correctamente, debemos añadir la siguiente linea en el fichero wp-config.php de nuestra web:

if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && $_SERVER['HTTP_X_FORWARDED_$
        $_SERVER['HTTPS']='on';

Añadiendo soporte para el formato de imágenes WebP

Aprovechando la ocasión, podemos incorporar otra mejora mas, y es la utilización del formato webp para nuestras imágenes.

El formato de imagen WebP, desarrollado por Google, permite reducir entre un 25 a un 34% el tamaño comparado con la misma imagen en formato jpg.

El inconveniente de WebP son principalmente dos:

  • He tenido que desactivar el modulo pagespeed. Con el modulo activado, no había manera que funcionase el reemplazo en vivo de las imágenes jpg y png por su equivalente WebP. Ni desactivando los filtros de imágenes, ni cambiando el modo de optimización de pagespeed.
  • No funciona en todos los navegadores. Según la web CanIUse.com, el soporte para WebP no existe en todos los navegadores, aunque si en los mas utilizados:

    Soporte de WebP en navegadores
    Soporte del formato de imagen WebP en los distintos navegadores

Modificación de Nginx para soportar WebP

Para que automaticamente, Nginx devuelva un fichero en formato WebP cuando se le solicita un fichero jpg o png, debemos hacer unos pequeños cambios en su configuración.

En el fichero /etc/nginx/mime.type, añadimos un nuevo tipo para WebP:

image/webp     webp;

Ademas, añadimos la siguiente linea en el fichero /etc/nginx/nginx.conf dentro del bloque http:

map $http_accept $webp_ext {
    default "";
    "~image\/webp" ".webp";
}

De esta manera, al recibir Nginx una solicitud de fichero png o jpg, comprueba si existe una imagen webp con el mismo nombre y devuelve esta.

Para automatizar la generación de ficheros webp, uso el plugin Media Webp. Este plugin nos permite generar las imágenes ya existentes en formato webp, ademas de generar automaticamente la imagen en formato webp al subirla a la biblioteca de medios de wordpress.

Configurar VPS desde cero VI: servidor web nginx y cache varnish ssl - Imagen 1
Plugin Media Webp para WordPress

Resultados: el antes y el después de la optimización

La verdad es que después de los cambios realizados se nota que la pagina carga bastante más rápido.

Se puede ver aquí una captura de pagespeed con los resultados de la version escritorio y móvil antes de realizar los cambios:

PageSpeed FernandezSanSalvador version pc antes de optimizar
PageSpeed FernandezSanSalvador version pc antes de optimizar
PageSpeed FernandezSanSalvador version movil antes de optimizar
PageSpeed FernandezSanSalvador version móvil antes de optimizar

Y esta es el resultado después de integrar Varnish y webp:

PageSpeed FernandezSanSalvador version pc despues de optimizar
PageSpeed FernandezSanSalvador version pc después de optimizar
PageSpeed FernandezSanSalvador version movil despues de optimizar
PageSpeed FernandezSanSalvador version móvil después de optimizar

Creo que es un nivel suficiente, ya que cuanto más alta sea la puntuación, más tiempo cuesta subirla hasta el máximo nivel. Creo que no merece la pena perder mas tiempo en optimizar, esta ya en un buen nivel y con una velocidad de carga de la web por encima de la media.

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *