Skip to content

Dockerizando Laravel o cómo crear un entorno de desarrollo para Laravel usando Docker

Máximo Martinez Soria

Jun 23, 2021 | 11 min to read |

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:

  1. Abajo de todo, afuera de services, definimos los volumes y creamos uno nuevo llamado dbdata. Es importante ponerle los : al final.
  2. 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.

Author

Máximo Martinez Soria