9.2.2.2.2.4. Ejemplos

Dedicaremos este último epígrafe a resolver algunos problemas usando contenedores de Docker.

9.2.2.2.2.4.1. dnsmasq

El propósito es ejecutar un contenedor con dnsmasq capaz de proporcionar resolución de nombres y configuración dinámica de direcciones a los equipos de la red. Esto último nos obliga a que el contenedor comporta la red con el anfitrión.

Aunque no usaremos docker-compose, puesto que sólo necesitamos un contenedor, Debemos preparar un directorio de trabajo con el siguiente contenido:

+ workdir
   +-- dnsmasq.d
   |     +-- dns.conf
   |     +-- dhcp.conf
   +-- Dockerfile

El subdirectorio dnsmasq.d contendrá la configuración que deseamos para nuestro dnsmasq. Por ejemplo:

# dns.conf
no-resolv
server=1.0.0.1
server=1.1.1.

y:

# dhcp.conf
log-dhcp
dhcp-range=192.168.255.64,192.168.255.127,5m
domain=internal.vm,192.168.255.0/24

Y el Dockerfile la forma en construir una imagen con dnsmasq a partir de Alpine:

FROM    alpine
RUN     apk update && apk add dnsmasq; \
        echo 'conf-dir=/etc/dnsmasq.d/,*.conf' > /etc/dnsmasq.conf
EXPOSE  53/UDP 67/UDP 69/UDP
CMD     ["dnsmasq", "--no-daemon", "-z"]

Con ello, podemos construir la imagen:

# docker build -t dnsmasq:alpine .

Y ejecutar el contenedor basado en esa imagen:

# docker run --rm --restart=always --network host -v /workdir/dnsmasq.d:/etc/dnsmasq.d dnsmasq:alpine

Obsérvese un aspecto importante en esta ejecución: la opción --restart que introduce cuál será la política de ejecución del contenedor, esto es, qué es lo que ocurre cuando el contenedor para. Hay cuatro posilibilidades:

Política

Descripción

no

Es el valor por defecto. El contenedor al parar, no reinicia.

on-failure

El contenedor sólo se reinicia si la aplicación acaba con un error.

always

Reinicia el contenedor siempre, pero si se detiene manualmente (con docker stop), sólo reinicia si se reinicia el demonio o si se reinicia manualmente.

unless-stopped

Como el caso anterior, pero no reinicia cuando el demonio se reinicia.

Tenga presente que el demonio se inicia, cuando el sistema anfitrión arraca. Por tanto, si nuestra política es always, el contenedor arrancará automáticamente, al arrancar el sistema anfitrión. Precisamente ese comportanamiento es el preferible en un contenedor que hemos creado para dar servicio DHCP a la red.

9.2.2.2.2.4.2. PHP-FPM con socket

Planteamos el objetivo de utilizar la imagen de PHP basada en Alpine, pero hacer accesible el servicio de FastCGI a través de un socket UNIX, en vez de un puerto TCP. Para ello tomaremos:

  • La imagen ya comentada, cuyo contenedor compartirá dos volúmenes:

    • Uno para albergar el socket.

    • Otro que contenga la aplicación.

  • Una segunda imagen de nginx cuyo contenedor compartirá los mismos dos volúmenes anteriores, más un tercero que contenga su configuración modular.

El directorio de trabajo será el siguiente:

+ workdir
    +-- docker-compose.yaml
    +-- nginx/
    |    +-- Dockerfile
    |    +-- archives/
    |         +-- conf.d
    |              +-- php.conf
    |              +-- default.conf
    +-- php-fpm/
    |    +-- Dockerfile
    |    +-- archives/
    |         +--etc/
    |             +-- php-fpm.d/
    |             |    +-- zz-docker.conf
    |             +-- php/
    |                  +-- conf.d/
    |                       +-- uploads.ini
    |                       +-- [.. otros ficheros ..]
    +-- webapp/
         +-- [.. ficheros de la aplicación ..]

Preparación de php-fpm

Básicamente, consiste en modificar la configuración de PHP para adaptarla a nuestras necesidades. Un cambio consiste en sustituir el fichero zz-docker.conf para lograr la comunicación a través de un socket. Su contenido es el siguiente:

[global]
daemonize = no

[www]
listen = /var/run/php-fpm/php-fpm.sock
listen.owner = www-data
listen.group = www-data

Además, dentro de etc/php/conf.d, podemos crear ficheros INI que alteren la configuración general de PHP. Por ejemplo, uno que aumente el tamaño de los ficheros subidos al servidor:

# conf.d/uploads.ini

post_max_size = 20M
upload_max_filesize = 20M

Por su parte, el fichero Dockerfile, por otra parte, debe ser:

ARG      version
FROM     php:${version}-fpm-alpine
COPY     ./archives /usr/local/

Preparación de nginx

Nuestro problema es que necesitamos que nginx lo ejecute el mismo usuario[1] que ejecuta php-fpm en el otro contenedor. php-fpm es ejecutado por el usuario www-data con UID 82 y cuyo grupo principal tiene un GID y nombre idénticos. nginx, en cambio, lo ejecuta el usuario nginx con un UID que no es 82. Solucionaremos el inconveniente creando en este contenedor el usuario www-data con UID 82 y haciendo que ejecute el servidor web.

Los ficheros de configuración del servidor son:

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

y:

# conf.d/default.conf
server {
   listen 80;

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

   location ~ \.php$ {
      fastcgi_split_path_info ^(.+\.php)(/.+)$;

      include fastcgi.conf;
      fastcgi_param PATH_INFO $fastcgi_path_info;

      fastcgi_pass php;
   }
}

y el Dockerfile:

FROM  nginx:alpine
RUN   adduser -Du82 -G www-data www-data; \
      sed -ri '/^user/s:nginx:www-data:' /etc/nginx/nginx.conf

docker-compose

Por último, el fichero para docker-compose que levante estos dos contenedores puede ser el siguiente:

version: "3"
services:
  php:
    image: php:fpm-alpine-socket
    build: ./php-fpm
    volumes:
      - socket:/var/run/php-fpm/
      - ./webapp:/srv/www
    restart: always

  nginx:
    image: nginx:alpine-app
    build: ./nginx
    ports:
      - "80:80"
    depends_on:
      - php
    volumes:
      - socket:/var/run/php-fpm/
      - ./webapp:/srv/www
      - ./nginx/conf.d:/etc/nginx/conf.d
    restart: always

volumes:
  socket:

Aplicación

En el directorio webapp debe colocarse la aplicación de deseemos ejecutar. Dado que únicamente queremos hacer una prueba nos basta con:

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

9.2.2.2.2.4.3. Wordpress

Nuestra intención ahora es instalar un Wordpress, para lo cual utilizaremos la siguiente infraestructura:

../../../../../_images/wordpress.png

es decir, tres contenedores diferentes cada uno de los cuales levanta los tres servicios en que se puede descomponer la aplicación: la base de datos, la aplicación PHP (con el intérprete incluido) y un servidor web que sea el que ofrezca la aplicación. Además, es necesario almacenar los ficheros de la base de la datos y los datos de la aplicación, por lo que se requerirán dos volúmenes de datos.

Nota

En este caso, y a diferencia del anterior ejercicio, la aplicación y PHP se encuentran en el mismo contenedor y, además, no se expone mediante socket, sino mediante TCP. No es muy complicado adaptar esta solución utilizando las estrategias del ejercicio anterior.

Los tres contenedores que utilizaremos son:

  • La imagen oficial de mariaDB, que se caracteriza porque al generar un contenedor, crea los ficheros necesarios del gestor de bases de datos, según los valores de las variables de entorno que se proporcionen (véase el docker-compose.yaml más adelante). Esta característica nos permite preparar la base de datos para wordpress y el usuario que la maneje.

  • Una imagen oficial de wordpress que incluya PHP-FPM. Esta imagen contiene el PHP necesario y la versión de Wordpress en el momento de su generación, la cual acaba dejando disponible en /var/www/html.

    Esta imagen también usa variables de entorno para conocer dónde se encuentra la base de datos y con qué usuarios acceder a ella.

  • Una imagen mínima de nginx que necesitará acceso al directorio /var/www/html del contenedor anterior y alterar su configuración predefinida para ser capaz de servir la aplicación.

Dado que actúan en comandita tres contenedores, lo más juicioso es utilizar docker-compose, el cual requerirá el siguiente docker-compose,yaml:

version: "3"
services:
   mysql:
      image: mariadb
      volumes:
         - wpmysql:/var/lib/mysql
      environment:
         MYSQL_ROOT_PASSWORD: ${ROOT_PASS:-toor}
         MYSQL_DATABASE: ${WP_DB:-wordpress}
         MYSQL_USER: ${WP_USER:-wp}
         MYSQL_PASSWORD: ${WP_PASS:-wp}
      restart: always

   wordpress:
      image: wordpress:php7.4-fpm-alpine
      depends_on:
        - mysql
      volumes:
        - wpapp:/var/www/html
      environment:
        WORDPRESS_DB_HOST: mysql
        MYSQL_ROOT_PASSWORD: ${ROOT_PASS:-toor}
        WORDPRESS_DB_NAME: ${WP_DB:-wordpress}
        WORDPRESS_DB_USER: ${WP_USER:-wp}
        WORDPRESS_DB_PASSWORD: ${WP_PASS:-wp}
        WORDPRESS_TABLE_PREFIX: ${WP_PREFIX:-wp_}
      restart: always

   nginx:
      image: nginx:alpine
      ports:
        - "80:80"
      volumes:
        - ./blogs.conf:/etc/nginx/conf.d/default.conf
        - wpapp:/var/www/html
      depends_on:
        - wordpress
      restart: always

volumes:
   wpmysql:
   wpapp:

Si se observa el fichero, se verá que es necesario suministrar el fichero blogs.conf con la configuración para que nginx sea capaz de ejecutar la aplicación:

+ workdir
    +-- blogs.conf
    +-- docker-compose.yaml

Su contenido puede ser este:

server {
   listen   80;
   root     /var/www/html;

   index index.php;

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

   location ~ \.php$ {
      fastcgi_split_path_info ^(.+\.php)(/.+)$;
      fastcgi_pass wordpress:9000;

      fastcgi_index index.php;
      include fastcgi.conf;

      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
      fastcgi_param SCRIPT_NAME $fastcgi_script_name;
   }
}

Notas al pie