Contenerizando aplicaciones web

Desarrollando una aplicación REST con Docker y Lumen

Guía desde 0 hasta un proyecto totalmente funcional con el framework REST basado en Laravel

Ariel Carvajal

--

Usar contenedores simplifica varios procesos tanto de desarrollo como de despliegue y nos provee de ventajas frente al proceso convencional. Por esto, en esta publicación mostraré como contenerizar una aplicación web REST construida con PHP 8 y Lumen 8.2, un framework basado en Laravel. Usaremos Postgres 13 como motor de base de datos e instalaremos herramientas como Swagger UI y PgAdmin para nuestro ambiente de desarrollo.

Docker cargando con nuestros contenedores
Imagen que describe la contenerización de nuestra aplicación

Manos a la obra!

Aviso: Algunos comandos pueden variar entre sistemas operativos, sobretodo las rutas. Para windows usualmente $PWD/ se debe escribir /$PWD/ y otras rutas tambien deben tener / como prefijo.

Instalando Docker

Para comenzar debemos instalar Docker CE (Community edition). Esto se puede hacer desde el sitio oficial.

Las instrucciones para cada plataforma están descritas en el link (en inglés).

Preparando el proyecto

Para comenzar crearemos un repositorio vacio en Github.

En mi caso le llamaré docker-lumen-example y puedes verlo en Github. Recomiendo que si clonas el proyecto que ya está terminado uses las instrucciones para instalar que se encuentran en el fichero README.md del repositorio.

Posterior a la creación del repositorio en Github, copiamos la URL y clonamos en nuestra computadora:

git clone git@github.com:uselessscat/docker-lumen-example.git

Esto creará una carpeta con el nombre del repositorio remoto, a la cual debemos movernos con cd. Todos los comandos se ejecutaran desde dentro de este directorio.

cd docker-lumen-example

Creando el proyecto de Lumen

Para crear el proyecto de lumen, debemos instalar Composer o podemos hacerlo de la manera Docker.

Vamos a Docker hub y buscamos composer, encontraremos que existe una imagen para usar composer sin instalarlo. Descargamos la imagen y luego instalamos Lumen.

docker pull composer
docker run --rm -ti -v $PWD:/app composer create-project --prefer-dist laravel/lumen src

El comando anterior crea un contenedor desechable que “sincroniza la terminal interna” de composer con la nuestra. Ademas monta nuestro directorio actual en un directorio llamado /app dentro del contenedor. Esto permite que composer ejecute el comando create-proyect --prefer-dist laravel/lumen src en nuestro disco por medio de la carpeta montada.

Puedes encontrar todos los detalles en la referencia de docker run y la documentación de la imagen de composer.

Probando que la instalación funciona

Para probar que el proyecto está instalado puedes ejecutar (solo si tienes PHP instalado):

php -S localhost:8000 -t src/public

Si no tienes instalado PHP no importa, mas adelante en este tutorial usaremos un contenedor que nos sirva para este fin.

Vamos a docker hub y buscamos PHP. Encontraremos la imagen oficial la cual usaremos para levantar nuestro proyecto. Luego elegimos alguna etiqueta conveniente para la imagen, en este caso para fines educacionales usaré el tag de Debian Buster con PHP 8.0 con interfaz de consola.

docker pull php:8.0-cli-buster

Entonces podemos correr el comando anterior pero esta vez dentro de un contenedor:

docker run --rm -ti -v $PWD/src:/src -p 8000:8000 php:8.0-cli-buster php -S 0.0.0.0:8000 -t /src/public

Entonces vamos a:

http://localhost:8000

Al ejecutar este comando se creará un contenedor usando la imagen de PHP con el tag 8.0-cli-buster que mapea nuestro puerto 8000 al puerto 8000 interno del contenedor donde PHP está mostrando el contenido de src. También es importante mencionar que la IP del comando PHP -S debe ser 0.0.0.0 para que sea accesible desde fuera del contenedor (en este caso, nuestra computadora).

Ambos comandos, tanto en local como en el contenedor deberían mostrar la version de Lumen

Lumen (8.2.1) (Laravel Components ^8.0)

Extendiendo la imagen de PHP

Para evitar tener que usar un comando tan largo, podemos extender la imagen de php anteriormente usada describiendo las operaciones nuevas en un fichero llamado dockerfile .

Creamos un fichero dockerfile en la raíz del proyecto con el siguiente contenido:

FROM php:8.0-cli-buster CMD php -S 0.0.0.0:8000 -t /src/public

Luego de crear el fichero, corremos los siguientes comandos:

docker build --tag=docker-lumen-example .docker run --rm -ti -v $PWD/src:/src -p 8000:8000 docker-lumen-example

El comando docker build, generará una nueva imagen a partir de Debian, pero con los parámetros que hemos proveído en el dockerfile .

Es importante mencionar que al momento de hacer un build de una imagen, docker toma el contexto de ficheros y lo pasa al contenedor en cuestión. Esta es la razón del punto al final del comando build . Con esto le decimos a docker que queremos usar nuestra carpeta actual como contexto de construcción. Si no quieres que algunos archivos se copien a la imagen, puedes usar un fichero llamado dockerignore para evitar que eso suceda.

Puedes ver la documentación de dockerfile para más información y tambien recomiendo leer la sección de dockerignore .

Preparando docker compose

Ya que sabemos que nuestra aplicación funciona, es tiempo de agregar Nginx y la base de datos.

Podríamos usar las imágenes de Nginx y de PostgreSql para crear 3 contenedores (incluido el de PHP) y conectarlas usando una red interna entre estos, pero tendremos que levantar y crear la red cada vez que clonemos el proyecto. Para evitar esto, se puede usar una herramienta de docker llamada docker-compose.

Docker compose permite correr aplicaciones multi-contenedor de manera simple. Para esto debemos definir un fichero llamado docker-compose.yml el cual se describen los servicios y contenedores que nuestra aplicación requiere para funcionar correctamente. Adicionalmente el fichero también describe las redes, volúmenes, imágenes, etc. que indican cómo se deben levantar y funcionar.

Abre la carpeta con tu editor preferido 🙊 y crea el fichero docker-compose.yml en la raíz del proyecto. En el fichero pon lo que sigue:

version: "3.8"
services:
php:
build:
context: .
volumes:
- ./src:/src
ports:
- 8000:8000

Ahora que docker compose está configurado, podremos levantar el proyecto con:

docker-compose up

El fichero anterior, compila la imagen de la misma forma que lo hemos hecho anteriormente con docker build, además monta src como volumen y expone el puerto 8000 del contenedor.

Agregando Nginx y PostgreSql

Para agregar Nginx y PostgreSql debemos declarar ambos servicios en el fichero de docker-compose. Debería quedar de la siguiente forma:

version: "3.8"
services:
nginx:
image: nginx:stable
volumes:
- ./src:/src
- ./configs/site.conf:/etc/nginx/conf.d/default.conf
working_dir: /src
ports:
- 80:80
networks:
default:
aliases:
- nginx
depends_on:
- php
php:
build:
context: .
dockerfile: dockerfile
volumes:
- ./src:/src
working_dir: /src
networks:
default:
aliases:
- php
depends_on:
- postgres
postgres:
image: postgres:13
restart: always
environment:
POSTGRES_DB: example
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
networks:
default:
aliases:
- db
networks:
default:

Los cambios que se agregan al docker-compose.yml permiten levantar 3 contenedores que actúan en una red interna mediante networks: default . Esto permite que las solicitudes que llegan a Nginx sean derivadas al contenedor de PHP mediante fpm y que también PHP pueda acceder a la base de datos. Podemos ver estos contenedores con:

docker container list

y la red creada con:

docker network list
docker network inspect docker-lumen-example_default

El resultado de estos comandos es el siguiente:

Resultados de los comandos anteriores

También debemos crear el fichero ./configs/site.conf con el siguiente contenido descrito por los docs de Laravel:

server {
server_name php-docker.local;
root /src/public;

index index.php index.html;

error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;

add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";

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

error_page 404 /index.php;

location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}

location ~ /\.(?!well-known).* {
deny all;
}
}

Y finalmente editar nuestro dockerfile para agregar el PDO de PostgreSql:

FROM php:8.0-fpm-buster

# install dependencies
RUN apt-get update \
&& apt-get install -y libpq-dev \
&& docker-php-ext-install -j$(nproc) pdo pdo_pgsql

Para levantar la nueva configuración debemos correr el comando:

docker-compose up --build

Con esto, ya podemos comenzar a trabajar en el código.

Agregando Swagger Ui y PgAdmin4

Para acceder a la base de datos y la documentación de la api, podemos agregar otros 2 servicios al docker-compose.yml . Estos están descritos por el siguiente fragmento:

pgadmin:
image: dpage/pgadmin4
environment:
PGADMIN_DEFAULT_EMAIL: "user@domain.com"
PGADMIN_DEFAULT_PASSWORD: "SuperSecret"
volumes:
- ${PWD}/configs/servers.json:/pgadmin4/servers.json
networks:
default:
aliases:
- pgadmin
ports:
- 81:80
depends_on:
- postgres
swagger:
image: swaggerapi/swagger-ui
ports:
- 82:8080
volumes:
- ./docs/swagger:/usr/share/nginx/html/definitions
environment:
API_URL: definitions/swagger.yml
depends_on:
- php

Esto agregado al final del fichero, deberá levantar ambos contenedores. Los cuales se pueden inspeccionar con docker container list :

Listado de contenedores levantados por docker-compose

Accediendo a los servicios

Los servicios que se levantaron con el docker compose tienen sus puertos abiertos definidos por el fichero de docker-compose.yml , si la configuración es la misma, deberían ser:

La base de datos está expuesta en el puerto 5432 para que se puedan conectar otras herramientas.

Para acceder a la base de datos mediante PgAdmin, debemos entrar a la url de localhost. Luego iniciar sesión con las credenciales especificadas en las variables de entorno dedocker-compose.yml

El fichero configs/servers.json contiene los datos de conexión a la base de datos, estas credenciales tambien están definidas en la seccion environment del servicio postgres en el fichero docker-compose .

Creando documentación con Swagger Ui

Para ingresar a swagger, debemos entrar a nuestro http://localhost:82. Veremos un mensaje que dice que no se ha podido cargar el fichero:

Esto es por que aun no hemos creado la documentación especificado por el volumen:

volumes:
- ./docs/swagger:/usr/share/nginx/html/definitions

Entonces creamos el documento swagger.yml con el siguiente contenido:

openapi: '3.0.3'
info:
title: Lumen Example
version: '1.0'
servers:
- url: http://localhost/
tags:
- name: Server info
description: Server information endpoints
paths:
/:
get:
tags:
- Server info
responses:
'200':
description: Lumen version

Con esto podemos documentar los endpoints para nuestra api rest. Puedes encontrar un ejemplo en Petstore y la documentación de open api

Conclusión

Luego de levantar el proyecto, quizás con algunas dificultades, hemos podido observar lo útil que es Docker para desplegar aplicaciones completas. Ahora cualquier integrante del equipo que quiera descargar el proyecto solo deberá correr docker-compose up y ya tendrá el ambiente de desarrollo listo :)

Espero haya sido de ayuda esta guía.

--

--