Dockerizando Laravel o cómo crear un entorno de desarrollo para Laravel usando Docker
Máximo Martinez Soria
Devops
Ahora que ya entendes el funcionamiento de Docker, y algunas de las herramientas que pone a nuestra disposición, es hora de poner todo esto en práctica. Para eso, vamos a crear un proyecto de Laravel y vamos a levantarlo con Docker.
Análisis del proyecto
Siempre que se empieza un proyecto, tenemos que pensar cuales son las herramientas que necesitaremos inicialmente para que todo funcione correctamente.
En nuestro caso, vamos a necesitar:
- Un sistema operativo: Linux.
- Un lenguaje de programación: Laravel está escrito en PHP.
- Un servidor: Nginx.
- Un motor de bases de datos: Mysql.
- Node: para usar SCSS/SASS y Vue.
Instalación del proyecto
Suficiente análisis, vamos a empezar con la parte divertida.
Crear proyecto de Laravel
Antes que nada, vamos a instalar Laravel. La forma más convencional es hacerlo con composer
, pero en este post estamos apuntando a que ni siquiera sea necesario tener PHP instalado para correr un proyecto en Laravel. Esa es la magia de Docker.
Por eso, en este caso vamos a hacer la instalación de la siguiente manera:
curl -s ""<https://laravel.build/example-app>"" | bash
Esto va a crear una carpeta example-app
con el proyecto de Laravel adentro. Sentite libre de renombrar la carpeta a tu gusto.
Las últimas versiones de Laravel, vienen con un archivo docker-compose.yml
. En ese archivo, definen los servicios necesarios para levantar el proyecto con Sail
. Una herramienta creada por el equipo de Laravel como una capa de abstracción por encima de Docker. En nuestro caso, queremos aprender a usar Docker, así que vamos a borrar ese archivo.
rm docker-compose.yml
Servidor
Para eso, lo primero que tenemos que hacer es buscar, en Docker Hub, la imagen de nginx
que vamos a usar.
Posteriormente, vamos a crear el archivo docker-compose.yml
. Como dijimos en el post de introducción a docker, no queremos tener que andar corriendo un comando por cada servicio que queremos levantar. Por eso usamos esta fantástica herramienta llamada docker compose
.
version: ""3.8""
services:
server:
image: nginx:alpine
restart: unless-stopped
ports:
- 81:80
- 444:443
networks:
- app-network
volumes:
- ./:/var/www
- ./nginx/conf.d/:/etc/nginx/conf.d/
networks:
app-network:
driver: bridge
Lo primero que tenemos que definir en el archivo docker-compose.yml
, es la versión de docker compose
. Esta siempre tiene que estar de forma explicita. En nuestro caso vamos a usar la 3.8, que es la última en este momento.
Una vez que tenemos configurada la versión, ya podemos empezar a definir las cosas necesarias para levantar nginx
. Para eso, vamos a definir un servicio, adentro de services
.
A este servicio vamos a llamarle server
, y vamos a decirle que cree un contenedor a partir de la imagen nginx:alpine
. También vamos a, adentro de la sección volumes
, decirle que haga un bind a la carpeta actual con la carpeta /var/www
del container, para que este pueda acceder a nuestro código. Y otro bind a la configuración de nginx
que está adentro de nuestro proyecto ./nginx/conf.d/
con la configuración de nginx
del container /etc/nginx/conf.d/
.
Por otro lado, necesitamos que este container pueda comunicarse con otros containers. Para eso, vamos a decirle que use la network app-network
. La cual definimos más abajo.
Sobre esa network se va a exponer el puerto 80. Ese puerto 80, es el puerto del container. No de nuestra computadora. Por lo cual, si tratamos de entrar al puerto 80 de localhost desde el browser, nos va a tirar un error.
Pero entonces, cómo hacemos para acceder desde el browser al container?
Para lograr esto, vamos a hacer un bind de puertos. Vamos a bindear un puerto de localhost con el puerto 80 del container. Eso se hace con la siguiente sintaxis: puerto-local:puerto-container
.
En mi caso use el puerto 81 de mi computadora y es importante que el puerto del container sea el 80. Ahora sí, ya podemos levantar el servicio con docker-compose up
y entrar a localhost:81 y acceder al container desde el browser.
Para terminar de configurar el server, vamos a escribir la configuración de nginx
en la ruta que definimos anteriormente ./nginx/conf.d/
, adentro del archivo app.conf
.
Tené en cuenta que tanto nginx
, como conf.d
son carpetas. Entonces tenemos que crear la carpeta nginx
, adentro creamos otra carpeta y la llamamos conf.d
y adentro creamos el archivo app.conf
con el siguiente contenido:
server {
listen 80;
index index.php index.html;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /var/www/public;
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass app:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
gzip_static on;
}
}
Base de datos
Para levantar Mysql
, vamos a definir un nuevo servicio llamado db
.
version: ""3.8""
services:
server:
...
db:
image: mysql:5.7.33
restart: unless-stopped
env_file:
- .env
environment:
MYSQL_DATABASE: $DB_DATABASE
MYSQL_ROOT_PASSWORD: $DB_PASSWORD
networks:
- app-network
volumes:
- dbdata:/var/lib/mysql
networks:
...
volumes:
dbdata:
La imagen va a ser mysql:5.7.33
. De esta forma podemos controlar la versión y no tener problemas en el futuro. Además, tenemos que agregar una variables de entorno para que la imagen defina la contraseña y el nombre de la base de datos que vamos a usar.
Con la opción env_file
, podemos decirle a docker-compose que lea nuestro archivo .env
. El mismo que usa Laravel. Una vez que le pasamos le decimos que archivo usar, podemos usar las variables definidas en ese archivo adentro de nuestro archivo yml.
Con esto en cuenta, vamos a definir, adentro de environment
, las variables MYSQL_DATABASE
y MYSQL_ROOT_PASSWORD
, y vamos a asignarlas a $DB_DATABASE
y $DB_PASSWORD
respectivamente. El $
hace referencia a que es una variable, y no un simple texto. Y como tenemos definido el archivo .env
, va a buscar las variables en este archivo.
Para conectarnos a la base de datos desde otros containers, tenemos que conectarla a la network. Eso lo hacemos, simplemente listando app-network
adentro de networks
como hicimos en el servicio de nginx
.
Por último, no queremos perder la información de la db cada vez que el container se apaga. Por eso, vamos a definir un volume
para guardar la info en él.
Esto lo hacemos en 2 pasos:
- Abajo de todo, afuera de
services
, definimos losvolumes
y creamos uno nuevo llamadodbdata
. Es importante ponerle los:
al final. - Volviendo a nuestro servicio
db
, vamos a crear un array de volumes y vamos a conectar nuestro volume con la carpeta en donde se encuentran los datos de la base de datos usando la misma sintaxis que usamos en los puertos:dbdata:/var/lib/mysql
PHP
El siguiente servicio que necesitamos, es PHP.
Para eso, necesitamos algunas cosas como ciertas dependencias que necesita Laravel para ejecutarse. Además, tenemos que instalar los paquetes de composer. Con lo cual, vamos a partir de la imagen de php, y vamos a crear una imagen custom.
Esto se hace en un archivo Dockerfile
. Así, sin extensión y con la D mayúscula.
Vamos a crear ese archivo a la misma altura que el archivo docker-compose.yml
con el siguiente contenido:
# Partimos de la imagen php en su versión 7.4
FROM php:7.4-fpm
# Copiamos los archivos package.json composer.json y composer-lock.json a /var/www/
COPY composer*.json /var/www/
# Nos movemos a /var/www/
WORKDIR /var/www/
# Instalamos las dependencias necesarias
RUN apt-get update && apt-get install -y \
build-essential \
libzip-dev \
libpng-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
libonig-dev \
locales \
zip \
jpegoptim optipng pngquant gifsicle \
vim \
git \
curl
# Instalamos extensiones de PHP
RUN docker-php-ext-install pdo_mysql zip exif pcntl
RUN docker-php-ext-configure gd --with-freetype --with-jpeg
RUN docker-php-ext-install gd
# Instalamos composer
RUN curl -sS <https://getcomposer.org/installer> | php -- --install-dir=/usr/local/bin --filename=composer
# Instalamos dependendencias de composer
RUN composer install --no-ansi --no-dev --no-interaction --no-progress --optimize-autoloader --no-scripts
# Copiamos todos los archivos de la carpeta actual de nuestra
# computadora (los archivos de laravel) a /var/www/
COPY . /var/www/
# Exponemos el puerto 9000 a la network
EXPOSE 9000
# Corremos el comando php-fpm para ejecutar PHP
CMD [""php-fpm""]
Cuando hacemos el COPY
de todos los archivos, no queremos copiar las carpetas vendor
y node-modules
. Agregarían muchísimo peso y no tiene sentido copiarlas ya que estas van a cambiar si los archivos donde se listan las dependencias cambian. Por eso, vamos a agregar un archivo .dockerignore
para decirle a Docker cuales son las carpetas/archivos que se tienen que ignorar. Funciona igual que el .gitignore
.
node_modules
vendor
Una vez que tenemos nuestra custom image, vamos a crear un servicio, llamado app
, que parta de ella en el archivo docker-compose.yml
.
version: ""3.8""
services:
server:
...
db:
...
app:
build: .
restart: unless-stopped
networks:
- app-network
volumes:
- ./:/var/www
networks:
...
volumes:
...
Como hicimos anteriormente, vamos a conectarlo a la network y a hacer un bind de la carpeta actual con /var/www/
.
En este caso, al build
vamos a pasarle un punto. Esto significa que la imagen de la cual parte el servicio, es un archivo llamado Dockerfile
que se encuentra en la misma carpeta que el archivo docker-compose.yml
.
Node
El último servicio que necesitamos es Node. Este lo vamos a usar para correr Webpack a través de Laravel Mix y así poder usar SCSS/SASS y Vue.
version: ""3.8""
services:
server:
...
db:
...
app:
...
node:
image: node:15-alpine
working_dir: /var/www
volumes:
- ./:/var/www
- /var/www/node_modules
command: sh /var/www/node_start.sh
depends_on:
- app
networks:
...
volumes:
...
En este punto, ya entendés todo lo que está pasando en este nuevo servicio. La única diferencia es que estamos ejecutando un archivo shell para poder correr algunos comandos.
En ese archivo, llamado node_start.sh
, simplemente vamos a correr install
y watch
:
#!/bin/sh
set -e
echo ‘Installing deps‘
npm install
echo ‘Watching changes‘
npm run watch
Correr comandos en el container
En este punto ya deberías tener todo funcionando. Simplemente hay que correr docker-compose up
y entrar a localhost:81.
Pero todavía nos faltan algunas cosas. Todavía no podemos correr comandos adentro del container. Con lo cual, no podemos usar la db, ni tinker.
Para correr comandos adentro del container, tenemos que referenciarlo desde docker-compose. De la siguiente manera:
# docker-compose exec nombre-de-servicio comando
docker-compose exec app php artisan migrate
docker-compose exec app php artisan tinker
Conclusión
Listo! Tenemos Laravel corriendo en Docker.
Por las dudas, te dejo el archivo docker-compose.yml
completo:
version: ""3.8""
services:
server:
image: nginx:alpine
restart: unless-stopped
ports:
- 81:80
networks:
- app-network
volumes:
- ./:/var/www
- ./nginx/conf.d/:/etc/nginx/conf.d/
db:
image: mysql:5.7.33
restart: unless-stopped
env_file:
- .env
environment:
MYSQL_DATABASE: $DB_DATABASE
MYSQL_ROOT_PASSWORD: $DB_PASSWORD
networks:
- app-network
volumes:
- dbdata:/var/lib/mysql
app:
build: .
restart: unless-stopped
networks:
- app-network
volumes:
- ./:/var/www
node:
image: node:15-alpine
working_dir: /var/www
volumes:
- ./:/var/www
- /var/www/node_modules
command: sh /var/www/node_start.sh
depends_on:
- app
networks:
app-network:
driver: bridge
volumes:
dbdata:
Lo único que tenemos que hacer para que todo el equipo tenga el mismo entorno, es subir al repo los archivos que creamos.