Instalar NextCloud usando NGINX, PHP-FPM, APCu, Redis y GlusterFS

Aprovechando que he montado un nuevo volumen Raid1 con dos discos duros Hitachi HGST_HTS721010A9E630 en mi servidor personal en Madrid he decidido migrar algunos contenedores desde el viejo volumen en Western Digital, especialmente porque el throughput del nuevo volumen de discos Hitachi es muy superior al volumen con discos Western Digital.

En algunos casos he optado por hacer instalaciones limpias, en lugar de mover el contenedor de volumen; como por ejemplo el contenedor que corre el nodo de NexctCloud en Madrid. Así que he aprovechado la ocasión para documentar el proceso en forma de post.

Algunas Consideraciones previas:

  1. El servicio de NextCloud está clústerizado en los nodos Madrid/Sevilla. Aunque debido a que la línea de Madrid es mucho mejor (300MB/300MB frente a 30MB/3MB), no se hace balanceo; por defecto todo el tráfico es enrutado hacia el nodo de Madrid, por lo que a efectos prácticos el servicio no dispone de alta disponibilidad (se precisa intervención humana para cambiar la DNS).
  2. Las comunicaciones entre los nodos de Madrid y Sevilla tiene lugar a través de un túnel VPN montado con OpenVPN.
  3. La base de datos es un clúster multimaster de MariaDB 10.1 con un nodo en Madrid y otro en Sevilla además de un Árbitro en Madrid para evitar situaciones de split-brain. Lo idóneo sería contar con una tercera ubicación donde colocar el Árbitro, aunque de momento con este sistema no he tenido problemas en 2 años a pesar de tener numerosas caídas, especialmente del nodo de Sevilla.
  4. El clúster multimaster de MariaDB esta alojado en otro contenedor, por lo que solo es necesario permitir el login desde la IP del nuevo contenedor de NextCloud; es decir, este punto no se toca en está entrada.
  5. La replicación a nivel de archivos se consigue a través de GlusterFS, con un brick en Madrid y otro en Sevilla.
  6. Los nodos del GlusterFS corren dentro del propio contenedor de NextCloud. Para evitar la caída del servicio se añadirá un nuevo nodo al clúster de GlusterFS, y una vez el nuevo servidor este configurado, sincronizado y operativo se eliminará el viejo nodo.
  7. Redis se usa como cache local para gestionar los bloqueos transaccionales, es decir, no existe un clúster de Redis por los motivos expuestos en el punto 1.

Preparar el SO

Añadir repositorios EPEL e IUS

En este caso se hará uso del repositorios IUS en lugar de REMI, ya que IUS no remplaza paquetes base del sistema, más información: The SafeRepo Initiative (IUS) y Replacement of the base packages? (REMI).

1
2
rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
rpm -Uvh https://centos7.iuscommunity.org/ius-release.rpm

Además será necesario añadir el repositorio de GlusterFS, en mi caso mi clúster usa los paquetes de la versión GlusterFS 3.8.

1
yum install centos-release-gluster38

Instalar paquetes

1
yum install crontabs nginx php71u-xml php71u-cli php71u-common php71u-gd php71u-process php71u-gd php71u-soap php71u-mbstring php71u-json php71u-opcache php71u-mysqlnd php71u-fpm redis32u php71u-pecl-redis php71u-pecl-apcu glusterfs-server

Configurar GlusterFS

Antes de poder añadir el servidor como nuevo nodo del clúster, es necesario iniciar el demonio y activar el inicio automático del demonio con el sistema:

1
2
systemctl start glusterd
systemctl enable glusterd

Establecer una relación de confianza con el nuevo nodo

Desde cualquiera de los nodos actuales de clúster GlusterFS es necesario indicar al clúster que hay un nuevo nodo y que deben de establecer una relación de confianza con él, esto se hace invocando gluster peer probe <host>, ejemplo:

1
2
[root@nextcloud01 ~]# gluster peer probe nextcloud03
peer probe: success.

Es posible comprobar que se ha añadido el nuevo nodo al clúster mediante gluster peer status, ejemplo:

1
2
3
4
5
6
7
8
9
10
[root@nextcloud01 ~]# gluster peer status
Number of Peers: 2
Hostname: nextcloud02
Uuid: 217ff42e-4a27-4fc8-b6db-b54fef3a8843
State: Peer in clúster (Connected)
Hostname: nextcloud03
Uuid: c8277eed-9d2d-4dc4-9700-3961093be06e
State: Peer in clúster (Connected)

Y ejecutado desde el nuevo nodo:

1
2
3
4
5
6
7
8
9
10
[root@nextcloud03 /]# gluster peer status
Number of Peers: 2
Hostname: nextcloud01
Uuid: cb0313f5-1864-438a-8679-da081045daa6
State: Peer in clúster (Connected)
Hostname: nextcloud02
Uuid: 217ff42e-4a27-4fc8-b6db-b54fef3a8843
State: Peer in clúster (Connected)

Añadir un nuevo brick al volumen GlusterFS

En terminología GlusterFS un brick es un volumen de almacenamiento alojado en un nodo (servidor), donde dicho nodo posee una relación de confianza con el resto de nodos del clúster. Es posible añadir nuevos brick al clúster a través del comando gluster volume add-brick <volume-name> replica <total-brick-number> <host>:<path>, donde:

  • <volume-name>, es el nombre del volumen de almacenamiento a replicar.
  • <total-brick-number>, es el número total de copias, de cada archivo, a mantener. Puesto que el objetivo es replicar los datos en todos los nodos, este número debe corresponder al número total de bricks para el volumen. En mi caso corresponde al número de nodos, ya que solo monto una vez el volumen en cada nodo.
  • <host>:<path>, indica el nuevo nodo a añadir así como la ruta absoluta donde deben ser almacenados los datos.

A continuación un ejemplo:

1
2
[root@nextcloud01 ~]# gluster volume add-brick nextcloud_data replica 3 nextcloud03:/data/nextcloud_data
volume add-brick: success

Es posible consultar el estado del volumen mediante gluster vol info <volume-name>, ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@nextcloud01 ~]# gluster vol info nextcloud_data
Volume Name: nextcloud_data
Type: Replicate
Volume ID: e7841fae-0420-4caf-9556-f6f3e70d2c46
Status: Started
Snapshot Count: 0
Number of Bricks: 1 x 3 = 3
Transport-type: tcp
Bricks:
Brick1: nextcloud01:/data/nextcloud_data
Brick2: nextcloud02:/data/nextcloud_data
Brick3: nextcloud03:/data/nextcloud_data
Options Reconfigured:
cluster.nufa: enable
cluster.data-self-heal-algorithm: diff
features.scrub-throttle: aggressive
features.scrub-freq: hourly
features.scrub: Inactive
features.bitrot: off
cluster.self-heal-daemon: enable
nfs.disable: on
performance.readdir-ahead: on
transport.address-family: inet

Configurar el sistema de cache

Lo recomendado es usar APCu para cacher en memoria las versiones compiladas de las páginas PHP y Redis para cachear el Bloqueo Transaccional de Archivos. Más información en Nextcloud 12 Server Administration Manual - Configuring Memory Caching y una pequeña comparación de APCu vs Redis en una Raspberry Pi en owncloudarchive/pi-image - Caching.

Configurar Redis

Puesto que Redis se usará solo desde la máquina local, he optado por un socket Unix-domain en lugar de un socket TCP/IP, es posible leer algunas pinceladas de la diferencia entre ambos tipos de sockets en el apartado socket Unix-domain vs socket TCP/IP del post Apache: Instalar y configurar PHP-FPM (FatsCGI PRocess Manager).

Para ello, editar el archivo de configuración /etc/redis.conf y ajustar los siguientes parámetros:

1
2
3
4
port 0
unixsocket /run/redis/redis.sock
unixsocketperm 770
supervised systemd

Donde,

  • port 0, hará que Redis no escuche en un socket TCP.
  • unixsocket, indica la ubicación del socket Unix-domain.
  • unixsocketperm, indica los permisos del socket Unix-domain. Lo normal es dar solo permisos de escritura al usuario, aunque este caso se han dado también al grupo ya que añadiremos el usuario php-fpm al grupo redis para que puede leer y escribir en el socket unix de Redis.
  • supervised systemd, indica a Redis que debe enviar READY=1 al $NOTIFY_SOCKET de systemd cuando este listo.

Por último iniciar el demonio y activar el inicio automático del demonio con el sistema:

1
2
systemctl enable redis
systemctl start redis

Configurar PHP-FPM 7.1

Editar el archivo de configuración /etc/php-fpm.d/www.conf y ajustar los siguientes parámetros:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
listen = /run/php-fpm/www.sock
listen.acl_users = nginx
chdir = /var/www/html
env[HOSTNAME] = $HOSTNAME
env[PATH] = /usr/local/bin:/usr/bin:/bin
env[TMP] = /tmp
env[TMPDIR] = /tmp
env[TEMP] = /tmp
php_value[opcache.enable]=1
php_value[opcache.enable_cli]=1
php_value[opcache.interned_strings_buffer]=8
php_value[opcache.max_accelerated_files]=10000
php_value[opcache.memory_consumption]=512
php_value[opcache.save_comments]=1
php_value[opcache.revalidate_freq]=1

Es muy importante no olvidar que el demonio php-fpm debe poder acceder tanto a los archivos PHP como al socket unix de Redis, por ello es necesario añadir al usuario php-fpm al grupo de nginx y redis.

1
2
usermod -aG nginx php-fpm
usermod -aG redis php-fpm

Si olvidamos el paso anterior, puesto que php-fpm no podrá escribir en el socket unix de Redis, en el log de php-fpm se registrarán errores similares a:

1
2017/07/22 13:09:24 [error] 863#0: *3 FastCGI sent in stderr: "Primary script unknown" while reading response header from upstream, client: 192.168.100.21, server: cloud.guillen.io, request: "GET /test/info.php HTTP/1.1", upstream: "fastcgi://unix:/var/run/php-fpm/www.sock:", host: "cloud.guillen.io"

Por último iniciar el demonio y activar el inicio automático del demonio con el sistema:

1
2
systemctl enable php-fpm
systemctl start php-fpm

Configurar NGINX

Hay que tener en cuenta que uso un HAProxy como SSL Offloading por lo que el servidor de nginx se limita a crear el php-handler y el virtualhost:

  1. php-handler -> Crear el archivo /etc/nginx/conf.d/0-php-handler.conf con el siguiente contenido:

    1
    2
    3
    upstream php-handler {
    server unix:/var/run/php-fpm/www.sock;
    }
  2. Virtualhost -> Crear el archivo /etc/nginx/conf.d/cloud.guillen.io.conf con el siguiente contenido:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    server {
    listen 80;
    server_name cloud.guillen.io;
    # Add headers to serve security related headers
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Robots-Tag none;
    add_header X-Download-Options noopen;
    add_header X-Permitted-Cross-Domain-Policies none;
    # Path to the root of your installation
    root /var/www/html/nextcloud/;
    location = /robots.txt {
    allow all;
    log_not_found off;
    access_log off;
    }
    location = /.well-known/carddav {
    return 301 $scheme://$host/remote.php/dav;
    }
    location = /.well-known/caldav {
    return 301 $scheme://$host/remote.php/dav;
    }
    # set max upload size
    client_max_body_size 2048M;
    fastcgi_buffers 64 4K;
    # Enable gzip but do not remove ETag headers
    gzip on;
    gzip_vary on;
    gzip_comp_level 4;
    gzip_min_length 256;
    gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
    gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;
    location / {
    rewrite ^ /index.php$uri;
    }
    location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)/ {
    deny all;
    }
    location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) {
    deny all;
    }
    location ~ ^/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater/.+|ocs-provider/.+)\.php(?:$|/) {
    fastcgi_split_path_info ^(.+\.php)(/.*)$;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    #Avoid sending the security headers twice
    fastcgi_param modHeadersAvailable true;
    fastcgi_param front_controller_active true;
    fastcgi_pass php-handler;
    fastcgi_intercept_errors on;
    fastcgi_request_buffering off;
    }
    location ~ ^/(?:updater|ocs-provider)(?:$|/) {
    try_files $uri/ =404;
    index index.php;
    }
    # Adding the cache control header for js and css files
    # Make sure it is BELOW the PHP block
    location ~ \.(?:css|js|woff|svg|gif)$ {
    try_files $uri /index.php$uri$is_args$args;
    add_header Cache-Control "public, max-age=15778463";
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Robots-Tag none;
    add_header X-Download-Options noopen;
    add_header X-Permitted-Cross-Domain-Policies none;
    # Optional: Don't log access to assets
    access_log off;
    }
    location ~ \.(?:png|html|ttf|ico|jpg|jpeg)$ {
    try_files $uri /index.php$uri$is_args$args;
    # Optional: Don't log access to other assets
    access_log off;
    }
    }

Configurar NextCloud

Cache

Se usará APCu para la cache de objetos locales y Redis para la cache del Bloqueo de Archivos Transacionales.
Editar el archivo /var/www/html/nextcloud/config/config.php y añadir:

1
2
3
4
5
6
7
8
'memcache.local' => '\OC\Memcache\APCu',
'memcache.locking' => '\\OC\\Memcache\\Redis',
'filelocking.enabled' => 'true',
'redis' => array(
'host' => '/run/redis/redis.sock',
'port' => 0,
'timeout' => 0.0,
),

Clean URL

  1. Añadir 'htaccess.RewriteBase' => '/', al archivo /var/www/html/nextcloud/config/config.php.
  2. Ejecutar su - nginx -c "php occ maintenance:update:htaccess" para recrear el archivo htaccess.

Tareas de NextCloud

Añadir las siguientes tareas programadas mediante el comando crontab -u nginx -e:

1
2
*/15 * * * * /usr/bin/php -f /var/www/html/nextcloud/cron.php
30 3 * * * /usr/bin/php -f /var/www/html/nextcloud/occ preview:pre-generate

Entradas de interés

Contenidos
  1. 1. Preparar el SO
    1. 1.1. Añadir repositorios EPEL e IUS
    2. 1.2. Instalar paquetes
  2. 2. Configurar GlusterFS
    1. 2.1. Establecer una relación de confianza con el nuevo nodo
    2. 2.2. Añadir un nuevo brick al volumen GlusterFS
  3. 3. Configurar el sistema de cache
    1. 3.1. Configurar Redis
  4. 4. Configurar PHP-FPM 7.1
  5. 5. Configurar NGINX
  6. 6. Configurar NextCloud
    1. 6.1. Cache
    2. 6.2. Clean URL
    3. 6.3. Tareas de NextCloud