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:
(O su primo hermanodocker compose up -d --wait
vendor/bin/sail up -d --wait
)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:
- Se utilizará el archivo
Dockerfile
ubicado envendor/laravel/sail/runtimes/8.4
- El proceso de construcción de la imagen requiere un argumento llamado
WWWGROUP
- 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:
- La imagen base que se usa es
ubuntu:24.04
(Link acá) - Es posible especificar, además de
WWWGROUP
, la versión de NodeJS, el cliente de MySQL y la versión de PostgreSQL - La versión de PHP que se utiliza es la 8.4
- Hay una cantidad de extensiones php instaladas (sqlite3, gd, mongodb, readline, etc…)
- 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:
- Descargar el código
- Levantar la aplicación localmente
- Estudiar los archivos de Docker
- Hacer algún pequeño cambio, por ejemplo agregar phpMyAdmin
- Si te animás, agregar alguna funcionalidad, ya sea al frontend o al backend
- Un ejemplo de Laravel React sobre Docker que funciona - 10/01/2025
- ¿Puede tener éxito una aplicación en PHP estructurado? - 06/01/2025
- Cómo enviarencabezados SOAP desde PHP - 09/12/2024