Un ejemplo de Laravel React sobre Docker que funciona

Cuando me llegó este mensaje

Necesito aprender a preparar entorno de desarrollo php, laravel y react con Docker

Mi primer instinto fue buscar algún tutorial para compartir. ¿Qué otra cosa podría ser mejor, no?

Y ahí me metí a buscar y probar unas cuantas opciones pero ninguna parecía funcionar.

Al menos no sin hacer unos cuantos ajustes y, dado que mi framework de referencia no es Laravel si no Symfony, me pareció que lo más lógico era seguir buscando.

Pues bien, hurgando por aquí y por allí llegué a este tutorial que me dio (casi) justo lo que estaba buscando.

Se trata de una sencilla app de manejo de usuarios:

Lo interesante: está desarrollada usando Laravel y React:

Y montada sobre Docker.

Si querés ver los detalles de cómo logré levantar la aplicación te recomiendo que leas el tutorial original aunque debo hacer una advertencia: tuve que hacer pequeños ajustes al código para dejarla funcionando pero, ahora que está ahí, podés descargar el código del repositorio y ejecutando estos comandos desde la raíz del proyecto:

  1. docker compose up -d --wait (O su primo hermano vendor/bin/sail up -d --wait)
  2. npm run dev

Deberías ser capaz de entrar a http://localhost y ser recibido por la aplicación.

El objetivo de este post no es analizar el propio código (Ni PHP ni React), si no centrarme en cómo se montó todo esto sobre Docker.

Para comprender lo que te voy a mostrar a continuación necesitás conocer los conceptos básicos de Docker. Si no los tenés frescos te recomiendo que, antes de seguir, busques aprender cómo funciona Docker en general (Tal vez este post sea un mejor lugar para arrancar).

¿Avanzamos?

Los archivos de docker

Primero que nada, una aclaración: Estos archivos los generó sail.

No es que eso tenga nada de malo, simplemente comentar que, si usás Laravel, no parece una mala idea apoyarte en este wrapper.

Ahora sí, el primer archivo a analizar no es otro que el docker-compose.yml

services:
    laravel.test:
        build:
            context: './vendor/laravel/sail/runtimes/8.4'
            dockerfile: Dockerfile
            args:
                WWWGROUP: '${WWWGROUP}'
        image: 'sail-8.4/app'
        extra_hosts:
            - 'host.docker.internal:host-gateway'
        ports:
            - '${APP_PORT:-80}:80'
            - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
        environment:
            WWWUSER: '${WWWUSER}'
            LARAVEL_SAIL: 1
            XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
            XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
            IGNITION_LOCAL_SITES_PATH: '${PWD}'
        volumes:
            - '.:/var/www/html'
        networks:
            - sail
        depends_on:
            - mysql
    mysql:
        image: 'mysql/mysql-server:8.0'
        ports:
            - '${FORWARD_DB_PORT:-3306}:3306'
        environment:
            MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
            MYSQL_ROOT_HOST: '%'
            MYSQL_DATABASE: '${DB_DATABASE}'
            MYSQL_USER: '${DB_USERNAME}'
            MYSQL_PASSWORD: '${DB_PASSWORD}'
            MYSQL_ALLOW_EMPTY_PASSWORD: 1
        volumes:
            - 'sail-mysql:/var/lib/mysql'
            - './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
        networks:
            - sail
        healthcheck:
            test:
                - CMD
                - mysqladmin
                - ping
                - '-p${DB_PASSWORD}'
            retries: 3
            timeout: 5s
networks:
    sail:
        driver: bridge
volumes:
    sail-mysql:
        driver: local

No te preocupes si te resulta algo intimidante, suele ser así cuando se ven por primera vez. Intentaré desmenuzarlo para que sea más sencillo el consumo.

Lo primero que encontramos es que hay dos servicios: laravel.test y mysql.

Analizaré cada uno por separado

El servicio laravel.test

Lo primero que se ve en la definición de este servicio es la clave build, lo que significa que se utilizará una imagen custom para crear el respectivo contenedor:

        build:
            context: './vendor/laravel/sail/runtimes/8.4'
            dockerfile: Dockerfile
            args:
                WWWGROUP: '${WWWGROUP}'

A partir de esta definición se pueden extraer las siguientes conclusiones:

  1. Se utilizará el archivo Dockerfile ubicado en vendor/laravel/sail/runtimes/8.4
  2. El proceso de construcción de la imagen requiere un argumento llamado WWWGROUP
  3. El valor del argumento de build se extrae de una variable de contexto llamada WWWGROUP

Veamos el contenido del Dockerfile para hacernos una idea más clara de qué tendrá la imagen y cómo se hará la construcción:

El archivo vendor/laravel/sail/runtimes/8.4/Dockerfile

FROM ubuntu:24.04

LABEL maintainer="Taylor Otwell"

ARG WWWGROUP
ARG NODE_VERSION=22
ARG MYSQL_CLIENT="mysql-client"
ARG POSTGRES_VERSION=17

WORKDIR /var/www/html

ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=UTC
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
ENV SUPERVISOR_PHP_USER="sail"

RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
    echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
    echo "Acquire::BrokenProxy    true;" >> /etc/apt/apt.conf.d/99custom

RUN apt-get update && apt-get upgrade -y \
    && mkdir -p /etc/apt/keyrings \
    && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano  \
    && curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
    && echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
    && apt-get update \
    && apt-get install -y php8.4-cli php8.4-dev \
       php8.4-pgsql php8.4-sqlite3 php8.4-gd \
       php8.4-curl php8.4-mongodb \
       php8.4-imap php8.4-mysql php8.4-mbstring \
       php8.4-xml php8.4-zip php8.4-bcmath php8.4-soap \
       php8.4-intl php8.4-readline \
       php8.4-ldap \
       php8.4-msgpack php8.4-igbinary php8.4-redis \
#       php8.4-swoole \
       php8.4-memcached php8.4-pcov php8.4-imagick php8.4-xdebug \
    && curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
    && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
    && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
    && apt-get update \
    && apt-get install -y nodejs \
    && npm install -g npm \
    && npm install -g pnpm \
    && npm install -g bun \
    && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
    && echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
    && curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \
    && echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
    && apt-get update \
    && apt-get install -y yarn \
    && apt-get install -y $MYSQL_CLIENT \
    && apt-get install -y postgresql-client-$POSTGRES_VERSION \
    && apt-get -y autoremove \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.4

RUN userdel -r ubuntu
RUN groupadd --force -g $WWWGROUP sail
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail

COPY start-container /usr/local/bin/start-container
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY php.ini /etc/php/8.4/cli/conf.d/99-sail.ini
RUN chmod +x /usr/local/bin/start-container

EXPOSE 80/tcp

ENTRYPOINT ["start-container"]

Creo que comentar línea por línea este archivo sería un poco demasiado así que me voy a centrar en las partes más relevantes:

  1. La imagen base que se usa es ubuntu:24.04 (Link acá)
  2. Es posible especificar, además de WWWGROUP, la versión de NodeJS, el cliente de MySQL y la versión de PostgreSQL
  3. La versión de PHP que se utiliza es la 8.4
  4. Hay una cantidad de extensiones php instaladas (sqlite3, gd, mongodb, readline, etc…)
  5. XDebug viene instalado

Respecto del argumento obligatorio WWWGROUP, se usa para establecer el id del grupo al que pertenecerá el usuario sail (el encargado de levantar el proceso php dentro del contenedor).

El objetivo de esto es evitar los problemas de permisos que podría haber cuando Laravel requiera escribir sobre archivos que deben ser accedidos por el servidor web.

El valor de este argumento lo provee sail.

Este Dockerfile es, por definición, bastante genérico, para ir a producción te convendría revisarlo bien.

En mi caso, por ejemplo, sólo utilizo MySQL, con lo cual, todo lo relativo a PostgreSQL y MongoDB no lo necesito.

A continuación vemos

        image: 'sail-8.4/app'

Lo que indica que la imagen que se generará llevará el nombre de sail-8.4/app

        extra_hosts:
            - 'host.docker.internal:host-gateway'

Esta definición agrega la capacidad de conectarse desde dentro del contenedor hacia el host, fundamental para que xdebug funcione.

        ports:
            - '${APP_PORT:-80}:80'
            - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'

En esta sección se establecen los mapeos entre los puertos del host y los del contenedor, de modo de que al acceder a localhost:{$APP_PORT} y localhost:{$VITE_PORT} se pueda realizar peticiones al servidor web y al vite respectivamente.

Los -80 y -5173 son los valores por defecto, es decir, los que se usarán a menos que haya otros especificados en variables de entorno (o en archivos .env).

        environment:
            WWWUSER: '${WWWUSER}'
            LARAVEL_SAIL: 1
            XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
            XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
            IGNITION_LOCAL_SITES_PATH: '${PWD}'

Esta parte define algunas variables de entorno que estarán presentes cuando el contenedor se levante.

El uso específico de cada una de ellas depende de lo que hagan los procesos instalados en el contenedor (el webserver principalmente).

        volumes:
            - '.:/var/www/html'

Esta sección define el punto de montaje del código de la aplicación hacia el directorio /var/www/html dentro del contenedor.

        networks:
            - sail

Aquí se define la red a la que estará conectado el contenedor (sail).

        depends_on:
            - mysql

Y por último, se define una dependencia entre el servicio laravel.test y mysql, lo que en la práctica quiere decir que el orden de inicio de los servicios será primero mysql y luego laravel.test.

El servicio mysql

El segundo servicio es para la base de datos, en mi caso MySQL:

        image: 'mysql/mysql-server:8.0'
        ports:
            - '${FORWARD_DB_PORT:-3306}:3306'
        environment:
            MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
            MYSQL_ROOT_HOST: '%'
            MYSQL_DATABASE: '${DB_DATABASE}'
            MYSQL_USER: '${DB_USERNAME}'
            MYSQL_PASSWORD: '${DB_PASSWORD}'
            MYSQL_ALLOW_EMPTY_PASSWORD: 1
        volumes:
            - 'sail-mysql:/var/lib/mysql'
            - './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
        networks:
            - sail
        healthcheck:
            test:
                - CMD
                - mysqladmin
                - ping
                - '-p${DB_PASSWORD}'
            retries: 3
            timeout: 5s

A simple vista podés notar que no hay una clave build, lo que significa que se usará una imagen estándar, en este caso mysql/mysql-server:8.0 (Link acá)

A través de la siguiente definición:

        environment:
            MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
            MYSQL_ROOT_HOST: '%'
            MYSQL_DATABASE: '${DB_DATABASE}'
            MYSQL_USER: '${DB_USERNAME}'
            MYSQL_PASSWORD: '${DB_PASSWORD}'
            MYSQL_ALLOW_EMPTY_PASSWORD: 1

Se establecen algunos valores necesarios para la incialización del servidor de base de datos.

        volumes:
            - 'sail-mysql:/var/lib/mysql'
            - './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'

Aquí se monta el volumen nombrado sail-mysql al directorio /var/lib/mysql del contenedor, con el objetivo de lograr persistencia de los datos.

Mediante el segundo mapeo se consigue crear la base de datos como puedes ver en vendor/laravel/sail/database/mysql/create-testing-database.sh:

#!/usr/bin/env bash

mysql --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL
    CREATE DATABASE IF NOT EXISTS testing;
    GRANT ALL PRIVILEGES ON \`testing%\`.* TO '$MYSQL_USER'@'%';
EOSQL

Aquí se define la pertenencia de este contenedor a la misma red (sail) que se utiliza en el servicio laravel.test para garantizar la conectividad entre ambos

        networks:
            - sail

Y mediante esta configuración:

        healthcheck:
            test:
                - CMD
                - mysqladmin
                - ping
                - '-p${DB_PASSWORD}'
            retries: 3
            timeout: 5s

Se establece un comando capaz de detectar si el servicio está funcional o no para tomar alguna acción al respecto o, al menos, poder informar de esta situación.

Finalmente nos encontramos con la definición de las redes:

networks:
    sail:
        driver: bridge

Y los volúmenes nombrados:

volumes:
    sail-mysql:
        driver: local

Qué hacer con todo esto

Este post, y el código que lo acompaña, están armados como un recurso didáctico, con lo cual, el mejor uso que les podés dar es estudiarlos a fondo.

Tu tarea pues es:

  1. Descargar el código
  2. Levantar la aplicación localmente
  3. Estudiar los archivos de Docker
  4. Hacer algún pequeño cambio, por ejemplo agregar phpMyAdmin
  5. Si te animás, agregar alguna funcionalidad, ya sea al frontend o al backend
mchojrin

Por mchojrin

Ayudo a desarrolladores PHP a afinar sus habilidades técnicas y avanzar en sus carreras

¿Te quedó alguna duda? Publica aca tu pregunta

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.