Introducción

En este post vamos a reaizar la implantación de una aplicación web PHP en docker. Vamos a hacer uso de Bookmedik. Crearemos una imagen Docker para implementar dicha aplicación.

Aspectos a tener en cuenta

  • Contenedor mariadb

    • Es necesario que nuestra aplicación guarde su información en un contenedor docker mariadb.
    • El script para generar la base de datos y los registros lo encuentras en el repositorio y se llama schema.sql. Deberemos crear un usuario con su contraseña en la base de datos. La base de datos se llama bookmedik y se crea al ejecutar el script.
    • Ejecuta el contenedor mariadb y carga los datos del script schema.sql. Para más información.
    • El contenedor mariadb debe tener un volumen para guardar la base de datos.
  • Contenedor bookmedik

    • Vamos a crear tres versiones de la imagen que nos permite implantar la aplicación PHP.
    • La imagen deberá crear las variables de entorno necesarias con datos de conexión por defecto.
    • Al crear un contenedor a partir de estas imágenes se ejecutará un script bash que realizará las siguientes tareas:
      • Que modifique el fichero core\controller\Database.php para que lea las variables de entorno. Para obtener las variables de entorno en PHP usar la función getenv. Para más información.
      • Que se inicialice la base de datos con el fichero schema.sql.
      • Que ejecute el servidor web.
    • El contenedor que crearemos debe tener un volumen para guardar los logs del servidor web.
    • La imagen la tenemos que crear en tu entorno de desarrollo con el comando docker build.

Preparamos el escenario

Lo primero que deberemos hacer es cerciorarnos de que tenemos instalado docker en nuestro entorno de desarrollo. Para ello, ejecutamos el siguiente comando:

$ docker --version

En caso de no tenerlo, lo instalaremos con el siguiente comando:

sudo apt install docker.io docker compose

Clonamos el repositorio de la aplicación web:

git clone https://github.com/evilnapsis/bookmedik.git

Tarea 1: Creación de una imagen docker con una aplicación web desde una imagen base

Llegados a este punto, y teniendo el repositorio forkado a nuestro usuario, vamos a modificar el fichero schema.sql para que se ejecute al crear el contenedor. Por ello, deberemos eliminar las siguientes líneas:

CREATE DATABASE bookmedik;
USE bookmedik;

Nos dirigimos al fichero core/controller/Database.php para que lea las variables de entorno. Para ello, debemos modificar las siguientes líneas:

<?php
class Database {
        public static $db;
        public static $con;
        function Database(){
                $this->user=getenv('USUARIO_BOOKMEDIK');$this->pass=getenv('CONTRA_BOOKMEDIK');$this->host=getenv('DATABASE_HOST');$this->ddbb=getenv('NOMBRE_DB');
        }

        function connect(){
                $con = new mysqli($this->host,$this->user,$this->pass,$this->ddbb);
                $con->query("set sql_mode=''");
                return $con;
        }

        public static function getCon(){
                if(self::$con==null && self::$db==null){
                        self::$db = new Database();
                        self::$con = self::$db->connect();
                }
                return self::$con;
        }
}
?>

Ya teniendo todo esto realizado, vamos a crear el fichero Dockerfile para crear la imagen docker. Para ello, debemos crear un fichero llamado Dockerfile en la raíz del proyecto y añadir el siguiente contenido:

FROM debian:bullseye
MAINTAINER María Jesús Alloza Rodríguez "mariajesus.allozarodriguez@gmail.com"
RUN apt-get update && apt-get upgrade -y && apt-get install apache2 libapache2-mod-php php php-mysql mariadb-client -y && apt-get clean && rm -rf /var/lib/apt/lists/*
COPY bookmedik /var/www/html/
ADD script.sh /opt/
RUN chmod +x /opt/script.sh && rm /var/www/html/index.html
ENTRYPOINT ["/opt/script.sh"]

Y el fichero script.sh:

#! /bin/sh

mysql -u $USUARIO_BOOKMEDIK --password=$CONTRA_BOOKMEDIK -h $DATABASE_HOST $NOMBRE_DB < /var/www/html/schema.sql

/usr/sbin/apache2ctl -D FOREGROUND

El script que hemos creado, tendrá como destino el directorio raíz del repositorio, para añadirlo al contenedor. Y ya podríamos crear la imagen del contenedor con el siguiente comando:

docker build -t legnakra/bookmedik:v1 .

Si ejecutamos docker images, podremos ver que la imagen se ha creado correctamente:

Tarea 2: Despliegue en el entorno de desarrollo

En esta tarea, crearemos un scritp con docker-compose que levantará el escenario con los contenedores. Debemos tener en cuenta que para acceder a la aplicación, las credenciales serán: Usuario: admin // contraseña: admin.

Por ello, creamos el fichero docker-compose.yaml para levantar ambos contenedores:

version: '3.8'
services:
  bookmedik:
    container_name: bookmedik-app
    image: legnakra/bookmedik:v1
    restart: always
    environment:
      USUARIO_BOOKMEDIK: admin
      CONTRA_BOOKMEDIK: admin
      DATABASE_HOST: bd_mariadb
      NOMBRE_DB: bookmedik
    ports:
      - 8081:80
    depends_on:
      - db
  db:
    container_name: bd_mariadb
    image: mariadb
    restart: always
    environment:
      MARIADB_ROOT_PASSWORD: root
      MARIADB_DATABASE: bookmedik
      MARIADB_USER: admin
      MARIADB_PASSWORD: admin
    volumes:
      - mariadb_data:/var/lib/mysql
volumes:
    mariadb_data:

Levantamos el escenario con docker compose up -dy con docker ps podemos ver que los contenedores se han levantado correctamente:

Si accedemos a la aplicación, podremos ver que funciona correctamente:

Tarea 3: Creación de una imagen docker con una aplicación web desde una imagen PHP

Ahora le toca el turno a la imagen de PHP. Para ello, crearemos un fichero llamado Dockerfile en la raíz del proyecto y añadiremos el siguiente contenido:

FROM php:7.4-apache-bullseye
MAINTAINER María Jesús Alloza Rodríguez "mariajesus.allozarodriguez@gmail.com"
RUN apt-get update && apt-get upgrade -y && docker-php-ext-install mysqli pdo pdo_mysql && apt-get install mariadb-client -y && apt-get clean && rm -rf /var/lib/apt/lists/*
ADD bookmedik /var/www/html/
ADD script.sh /opt/
RUN chmod +x /opt/script.sh
ENTRYPOINT ["/opt/script.sh"]

Y creamos una nueva imagen:

docker build -t legnakra/bookmedik:v2 .

EL fichero docker-compose.yaml quedaría de la siguiente forma:

version: '3.8'
services:
  bookmedik:
    container_name: bookmedik-app
    image: legnakra/bookmedik:v2
    restart: always
    environment:
      USUARIO_BOOKMEDIK: bookmedik
      CONTRA_BOOKMEDIK: bookmedik
      DATABASE_HOST: bd_mariadb
      NOMBRE_DB: bookmedik
    ports:
      - 8081:80
    depends_on:
      - db
  db:
    container_name: bd_mariadb
    image: mariadb:latest
    restart: always
    environment:
      MARIADB_ROOT_PASSWORD: root
      MARIADB_DATABASE: bookmedik
      MARIADB_USER: bookmedik
      MARIADB_PASSWORD: bookmedik
    volumes:
      - mariadb_data:/var/lib/mysql
volumes:
    mariadb_data:

Realizamos el despliegue con docker-compose up -d y como vemos en la imagen anterior, ambos contenedores se están ejecutando y en la siguiente, comprobamos que la imagen se ha creado correctamente:

Si accedemos a la aplicación, podremos ver que funciona correctamente:

Tarea 4: Ejecución de una aplicación PHP en docker con nginx

En esta tarea, vamos a tener que crear dos imágenes:

  • Una que contrendrá la aplicación PHP y será nginx la encargada de servirla.
  • Otra que tendrá los modulos de PHP necesarios para que la aplicación funcione.

Creamos el Dockerfile con php-fpm y sus correspondientes modulos:

FROM php:7.4-fpm
MAINTAINER María Jesús Alloza Rodríguez "mariajesus.allozarodriguez@gmail.com"
RUN docker-php-ext-install mysqli

Creamos la imagen:

docker build -t legnakra/php-fpm-mysql:v1 .

Para crear el dockerfile con la aplicación y nginx como servidor web, añadimos el siguiente contenido:

FROM nginx
MAINTAINER María Jesús Alloza Rodríguez "mariajesus.allozarodriguez@gmail.com"
RUN apt-get update && apt-get upgrade -y && apt-get install mariadb-client -y && apt-get clean && rm -rf /var/lib/apt/lists/*
ADD default.conf /etc/nginx/conf.d/
ADD bookmedik /usr/share/nginx/html
ADD script.sh /opt/
RUN chmod +x /opt/script.sh && rm /usr/share/nginx/html/index.html
ENTRYPOINT ["/opt/script.sh"]

En el docker file anterior, hemos establecido el fichero default.conf como el fichero de configuración de nginx. Este fichero contendrá la siguiente información:

nano default.conf

server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;
    error_log  /var/log/nginx/error.log;
    access_log /var/log/nginx/access.log;
    root   /usr/share/nginx/html;
    index  index.php index.html;

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass book_php:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
}

Y también deberemos modificar script.sh para adaptarlo a nginx:

#! /bin/sh

sleep 10

mysql -u $USUARIO_BOOKMEDIK --password=$CONTRA_BOOKMEDIK -h $DATABASE_HOST $NOMBRE_DB < /usr/share/nginx/html/schema.sql

nginx -g "daemon off;"

Creamos la imagen:

docker build -t legnakra/bookmedik:v3 .

Antes de modificar el docker-compose.yaml debemos tener en cuenta que:

  • Los contenedores que tienen php-fpm y nginx, deben estar en la misma ruta.
  • Las variables de entorno que se pasan al contenedor de nginx, deben también estar en el contenedor de php-fpm.

Por lo que el fichero docker-compose.yaml quedaría de la siguiente forma:

version: '3.8'
services:
  bookmedik:
    container_name: bookmedik-app
    image: legnakra/bookmedik:v3
    restart: always
    environment:
      USUARIO_BOOKMEDIK: bookmedik
      CONTRA_BOOKMEDIK: bookmedik
      DATABASE_HOST: bd_mariadb
      NOMBRE_DB: bookmedik
    ports:
      - 8082:80
    depends_on:
      - db
      - php
    volumes:
      - phpdocs:/usr/share/nginx/html/
  db:
    container_name: bd_mariadb
    image: mariadb
    restart: always
    environment:
      MARIADB_ROOT_PASSWORD: root
      MARIADB_DATABASE: bookmedik
      MARIADB_USER: bookmedik
      MARIADB_PASSWORD: bookmedik
    volumes:
      - mariadb_data:/var/lib/mysql
  php:
    container_name: book_php
    image: legnakra/php-fpm-mysql:v1
    restart: always
    environment:
      USUARIO_BOOKMEDIK: bookmedik
      CONTRA_BOOKMEDIK: bookmedik
      DATABASE_HOST: bd_mariadb
      NOMBRE_DB: bookmedik
    volumes:
      - phpdocs:/usr/share/nginx/html/ 

volumes:
    mariadb_data:
    phpdocs:

Tras todos los pasos anteriores, podemos cerciorarnos en la imagen anterior, que las imágenes que hemos confeccionado se han creado correctamente.

Ahora, ejecutamos el docker compose.yaml:

docker-compose up -d

Y comprobamos que todo funciona como esperábamos:

Si accedemos a la aplicación, podremos ver que funciona correctamente:

Tarea 5: Puesta en producción de nuestra aplicación

Vamos a poner en producción la aplicación a través de nuestro VPS, por lo que lo primero que deberemo de hacer es crear un registro CNAME en nuestro DNS para que apunte a la IP de nuestro VPS.

⬜️ bookmedik.mariatec.es CNAME mariatec.es

Y le generamos un certificado de Let's Encrypt:

certbot certonly --standalone -d bookmedik.mariatec.es

Instalamos docker y docker-compose en nuestro VPS:

sudo apt update && sudo apt upgrade -y && sudo apt install docker.io docker-compose-plugin -y

Al tener las imágenes creadas subidas a Docker Hub, podemos descargarlas en nuestro VPS:

docker pull legnakra/bookmedik:v2

En mi caso he elegido la versión 2, ya que es la que tiene el fichero docker-compose.yaml adaptado para el VPS.

Creamos el virtualhost en nginx para que actúe de proxy inverso:

nano /etc/nginx/sites-available/bookmedik.mariatec.es
server {
        listen 80;
        listen [::]:80;

        server_name bookmedik.mariatec.es;

        return 301 https://$host$request_uri;
}

server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;

        ssl    on;
        ssl_certificate /etc/letsencrypt/live/bookmedik.mariatec.es/fullchain.pem;
        ssl_certificate_key     /etc/letsencrypt/live/bookmedik.mariatec.es/privkey.pem;

        index index.html index.php index.htm index.nginx-debian.html;

        server_name bookmedik.mariatec.es;

        location / {
                proxy_pass http://localhost:8083;
                include proxy_params;
        }

}

Creamos el enlace simbólico y reiniciamos nginx:

ln -s /etc/nginx/sites-available/bookmedik /etc/nginx/sites-enabled/
systemctl restart nginx

En mi caso, he subido los ficheros a github, por lo que he clonado el repositorio y el repositorio de Bookmedik en mi VPS, dejando la estructura de carpetas de la siguiente forma:

├── Bookmedik
│   ├── docker-compose.yaml
│   ├── Dockerfile
│   ├── scripts
│   ├── bookmedik
│   │   ├── PhpWord
│   │   ├── core
│   │   ├── assets
│   │   ├── report
│   │   ├── index.php
│   │   ├── instalation.txt
│   │   ├── schema.sql
│   │   ├── logout.php
│   │   ├── README.md

Realizamos la ejecución del comando docker-compose up -d y comprobamos que todo funciona correctamente:

Y como podemos comprobar en la siguiente imagen, la aplicación funciona correctamente.

NOTA: A la hora de hacer el despliegue, debemos cerciorarnos de cambiar el puerto de escucha, dado que puede caber la posibilidad (o que sea muy probable) de que el puerto 8080 esté ocupado. En mi caso, he puesto el puerto de escucha en el 8083 debido a que ya tengo aplicaciones desplegadas en mi VPS.

Tarea 6: Modificación de la aplicación

Vamos a modificar la aplicación para que nos muestre nuestro nombre en la línea <h4 class="title">Acceder a BookMedik</h4>.

Para ello, vamos a utilizar la imagen de legnakra/bookmedik:v1 que ya tiene el fichero docker-compose.yaml para generar la nueva imagen.

nano `core/app/view/login-view.php`
---
<h4 class="title">María Jesús Alloza Rodríguez</h4>

Creamos la nueva imagen

docker build -t legnakra/bookmedik:v1_2 .

La resubimos a DockerHub:

docker push legnakra/bookmedik:v1_2

Eliminamos los contenedores que están en ejecución, modificamos el fichero docker-compose.yaml para que descargue la nueva imagen y ejecutamos el docker-compose.yaml:

docker compose down
nano docker-compose.yaml
version: '3.8'
services:
  bookmedik:
    container_name: bookmedik-app
    image: legnakra/bookmedik:v1_2
    restart: always
    environment:
      USUARIO_BOOKMEDIK: admin
      CONTRA_BOOKMEDIK: admin
      DATABASE_HOST: bd_mariadb
      NOMBRE_DB: bookmedik
    ports:
      - 8081:80
    depends_on:
      - db
  db:
    container_name: bd_mariadb
    image: mariadb
    restart: always
    environment:
      MARIADB_ROOT_PASSWORD: root
      MARIADB_DATABASE: bookmedik
      MARIADB_USER: admin
      MARIADB_PASSWORD: admin
    volumes:
      - mariadb_data:/var/lib/mysql
volumes:
    mariadb_data:

Y con esto daríamos terminado como desplegar una aplicación PHP en un VPS con contenedores Docker.

Conclusiones

Docker es una herramienta muy útil para el despliegue de aplicaciones, ya que nos permite crear contenedores que contienen todo lo necesario para que la aplicación funcione correctamente. Además, nos permite crear imágenes que podemos subir a Docker Hub para que otras personas puedan descargarlas y utilizarlas.

Bibliografía

https://www.docker.com/

https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04-es