Volver al blog

Desplegando Laravel, Nginx y MySQL con Docker Compose

Desplegando Laravel, Nginx y MySQL con Docker Compose

Introducción

La integración continua (CI) y el despliegue continuo (CD) son algunos de los temas más populares en el desarrollo de software actualmente. Para lograr el aspecto de CI/CD de la arquitectura de software, los desarrolladores hacen uso de contenedores. Los contenedores son entornos ligeros, virtualizados, portátiles y definidos por software. En los contenedores, el software puede ejecutarse de forma aislada de otro software que se ejecute en la máquina host física. El enfoque de este tutorial es el uso de la plataforma de contenedores Docker para desplegar y ejecutar aplicaciones web. Docker ayuda a agilizar el proceso de configuración de una pila de servidor web. En este tutorial, utilizaremos la pila LEMP para servir una aplicación Laravel .

La pila LEMP combina Linux como sistema operativo, Nginx como servidor web, MySQL como base de datos y PHP como lenguaje para scripting y procesamiento dinámico. Puede seguir nuestro tutorial sobre cómo instalar y configurar una pila LEMP en Ubuntu. Laravel es uno de los principales frameworks de PHP para el desarrollo de aplicaciones web.

Docker proporciona una herramienta llamada Docker Compose para definir el proceso de configuración de un contenedor Docker. Docker Compose permite a los desarrolladores definir la infraestructura de su aplicación, servicios, volúmenes, redes y cualquier dependencia en un solo archivo llamado archivo docker-compose. Puede administrar múltiples contenedores Docker a través de sus comandos como docker container create, docker container run, etc.

En este tutorial, aprenderá cómo desplegar una aplicación web Laravel con Nginx y MySQL dentro de un contenedor Docker. Las configuraciones para toda la pila se definirán dentro de un archivo docker-compose, así como otros archivos de configuración para PHP, MySQL y Nginx. ¡Comencemos!

Lo primero es lo primero

Paso 1: Descargar Laravel e instalar las dependencias

El primer paso es obtener el código de Laravel de un repositorio. En escenarios reales, es posible que tenga su código de Laravel en algún repositorio, por ejemplo, en GitHub, Bitbucket, GitLab, etc. Sin embargo, para los fines de este tutorial, clonaremos la última versión del repositorio oficial de Laravel en GitHub. El repositorio viene con un archivo composer, un gestor de dependencias a nivel de aplicación para PHP. Dado que queremos que todo se ejecute dentro del contenedor Docker, instalaremos las dependencias utilizando la imagen composer de Docker. Esto también nos ayudará a evitar tener que instalar composer globalmente en la máquina host real. A continuación, abra su terminal.

Cambie a su directorio de inicio:

Introduzca el siguiente comando para clonar el repositorio en un directorio llamado laravel-web. Es libre de nombrarlo como desee. En el momento de escribir este repositorio, al ejecutar este comando, se obtiene la versión 8 de Laravel. Cuando ejecute el comando, probablemente encontrará una nueva versión:

A continuación, muévase al directorio en el que acaba de clonar el repositorio:

Introduzca el siguiente comando para montar los directorios necesarios para su aplicación Laravel utilizando la imagen composer de Docker:

Las banderas -v y –rm en el comando docker run crean un contenedor efímero que se montará de forma vinculada (bind-mount) al directorio actual antes de ser eliminado. El comando copia el contenido de ~/laravel-web en el contenedor y garantiza que la carpeta vendor creada por composer se vuelva a copiar en el directorio actual.

Ahora, debe cambiar la propiedad del directorio laravel-web al usuario no raíz. Esto permitirá trabajar con el código de su aplicación como un usuario no raíz y ejecutar procesos dentro del contenedor en los pasos siguientes. Ingrese el siguiente comando para cambiar la propiedad:

El código de su aplicación ya está en su lugar. El directorio es propiedad de un usuario no raíz, por lo que puede proceder a definir los servicios de la aplicación en el archivo docker-compose.

Paso 2: Crear el archivo Docker Compose

Docker Compose simplifica el proceso de creación y despliegue de una aplicación. Una vez que haya definido las configuraciones y los servicios, puede desplegar fácilmente su aplicación en cualquier máquina host que tenga instalados Docker y Docker Compose sin preocuparse por las dependencias de la aplicación. Lo más importante es que puede hacer esto con un solo comando de Docker Compose, como veremos en el Paso 9.

En este paso, definirá un archivo Docker Compose con las configuraciones para el servidor web, la base de datos y los servicios de la aplicación necesarios para desplegar una aplicación Laravel.

Los archivos de Docker Compose son YAML archivos guardados con la extensión .yml. Tenga en cuenta que es necesaria una sangría adecuada para que el archivo Docker Compose sea válido. Ingrese el siguiente comando para crear y abrir el archivo con nano para editarlo:

A continuación, definirá tres servicios en este archivo: app, webserver y db. La sección db define las credenciales de la base de datos para su aplicación, así que asegúrese de elegir una contraseña mysql_root_password segura y reemplazarla en esa sección. Copie y pegue el siguiente código:

A continuación, se presentan las explicaciones de las definiciones de los servicios del código anterior:

  • app: Define la aplicación Laravel y ejecuta una imagen Docker personalizada, cloudsigma.com/php, que definiremos en el Paso 4. También establece el working_dir en el contenedor a /var/www/html.
  • webserver: Descarga la imagen nginx:alpine de Docker y expone los puertos 80 y 443.
  • db: Descarga la imagen imagen mysql:5.7.32 de Docker y define algunas variables de entorno. Estas incluyen una base de datos llamada laravel_web para la aplicación y la contraseña de root para la base de datos. Puede cambiar el nombre de la base de datos por el de su elección. Recuerde reemplazar la propiedad MYSQL_ROOT_PASSWORD con una contraseña segura. Este servicio también mapea el puerto 3306 en el host al puerto 3306 en el contenedor.

La propiedad container_name en cada servicio define un nombre para el contenedor correspondiente al servicio. Si no define la propiedad, Docker elige un nombre aleatorio para cada contenedor.

La propiedad networks define una red bridge llamada app-network que facilita la comunicación entre contenedores. Una red bridge está controlada por un puente de software que solo permite la comunicación entre contenedores en el mismo puente de red. El controlador de software del puente instala controladores que evitan que los contenedores en diferentes redes bridge se comuniquen directamente entre sí. Esto garantiza un alto nivel de seguridad, ya que solo los servicios relacionados pueden comunicarse directamente. Puede optar por definir múltiples servicios y redes que se conecten a funciones relacionadas.

Paso 3: Cómo persistir datos

Su aplicación web manejará y servirá datos a sus usuarios. En este paso, lo guiaremos a través de la definición de volúmenes y bind mounts para las definiciones de sus servicios para persistir los datos de la aplicación. Docker ofrece características increíbles como bind mounts y volúmenes para persistir datos y guardar archivos de configuración de la aplicación. Los usaremos para configurar nuestra aplicación Laravel con Docker.

Se prefieren los volúmenes por varias razones, incluido el ofrecimiento de copias de seguridad y la persistencia de datos más allá del ciclo de vida de un contenedor. Los Bind Mounts generalmente hacen referencia a un directorio real en la máquina host. Cuando crea un volumen, se crea un nuevo directorio dentro del directorio de almacenamiento de Docker que es administrado por Docker. Cuando crea un bind mount, un archivo o directorio dentro de la máquina host se monta en un contenedor (referenciado por su ruta absoluta). Esto es crucial para nuestra aplicación web porque cuando realiza cambios en el código en la máquina host, estos estarán disponibles de inmediato para el contenedor.

Tenga cuidado al usar bind mounts. Los procesos que se ejecutan dentro del contenedor de Docker pueden realizar cambios en el sistema de archivos del host y afectar a los procesos que no son de Docker que se ejecutan en el sistema host. Aunque los montajes de Docker son una característica potente, tenga en cuenta estas implicaciones de seguridad.

Dicho esto, veamos cómo podemos usar estas dos características en nuestra configuración. Primero, definiremos un volumen para persistir la base de datos MySQL. En el archivo Docker Compose que habíamos creado, bajo el servicio db, agregue una propiedad volumes como se destaca a continuación:

Como se definió, el volumen dbdata persistirá el contenido de /var/lib/mysql. Facilita las copias de seguridad y permite reiniciar el servicio sin perder los datos. A continuación, debe agregar la definición de volúmenes al final del archivo Docker Compose para que esté disponible en todos los servicios. Ingrese el siguiente fragmento de código en la parte inferior del archivo:

Para conectarse a una base de datos MySQL, debe proporcionar credenciales. Para hacerlo, defina un bind mount agregando el siguiente fragmento de código destacado al servicio db bajo la propiedad volumes:

El código vincula ~/laravel-web/mysql/my.cnf a /etc/mysql/my.cnf en el contenedor. El archivo vinculado son los archivos de configuración de MySQL que crearemos en el Paso 7.

El contenedor necesita usar el servidor Nginx para servir el código de su aplicación. Por lo tanto, definiremos dos bind mounts (uno para el archivo de configuración de Nginx y otro para el código de la aplicación), bajo el servicio webserver para este propósito. Agregue el siguiente fragmento de código para la definición de volúmenes bajo el servicio webserver:

Esta línea – ./:/var/www/html vincula el código de la aplicación en el directorio ~/laravel-web al directorio /var/www/html dentro del contenedor. Para el segundo montaje de tipo bind, se creará un archivo de configuración para Nginx en ~/laravel-web/nginx/conf.d/. Se montará en /etc/nginx/conf.d/ dentro del contenedor. Por lo tanto, puede actualizar el archivo de configuración en la máquina host según sea necesario. Crearemos el archivo de configuración de Nginx en Paso 6.

Para que los cambios en el código se reflejen automáticamente en el contenedor, vinculamos mediante un montaje de tipo bind el código de la aplicación al contenedor. Esto acelera el proceso de despliegue. Por lo tanto, agregue el siguiente fragmento de código resaltado al servicio app:

La segunda línea vincula un archivo de configuración de PHP, que crearemos en el Paso 5 dentro del archivo ~/laravel-web/php/laravel.ini a /usr/local/etc/php/conf.d/laravel.ini dentro del contenedor.

Su archivo Docker Compose completo ahora debería verse así:

Si todo se ve bien, presione Ctrl + O para guardar el archivo. Luego, presione Ctrl + X para salir del editor. En este punto, debería poder compilar una imagen personalizada de Docker para su aplicación con el archivo Docker Compose.

Paso 4: Crear el Dockerfile

Dockerfile incluye instrucciones que Docker puede usar para compilar imágenes personalizadas de Docker. También puede instalar el software requerido y configurar los ajustes necesarios para su aplicación. Especifican el entorno dentro de un contenedor que alojará el código de su aplicación. Puede subir las imágenes que cree a docker hub para compartirlas o colocarlas en otros registros privados.

Crearemos un Dockerfile que especificará las instrucciones para compilar la imagen de la aplicación Laravel. Use nano para crear el Dockerfile en el directorio ~/laravel-web:

En el editor abierto, agregue el siguiente código:

El Dockerfile primero crea una imagen basada en la php:7.4-fpm de Docker. Esta es una imagen basada en Debian con la implementación de PHP FastCGI (PHP-FPM instalada. Para que Laravel funcione correctamente, requiere que estén disponibles otras extensiones de php como mcrypt, pdo_mysql, mbstring e imagick, las cuales instala el script. Luego instala el composer gestor de paquetes php. El contenedor lo utilizará para instalar las dependencias php de Laravel.

Puede utilizar la directiva RUN para definir comandos como instalar, actualizar y configurar ajustes dentro del contenedor. También asigna permisos de usuario. La directiva WORKDIR especifica el directorio de trabajo, /var/www/html en este caso. El script ejecuta el comando CHOWN para asignar los permisos del directorio /var/www/html al usuario www-data.

Antes de compilar finalmente la imagen, se debe exponer un puerto para permitir el acceso a la aplicación que se ejecuta dentro del contenedor. El comando EXPOSE expone un puerto, el 9000, para el servidor php-fpm. El comando final a ejecutar es la directiva CMD. Ejecuta php-fpm para iniciar el servidor.

Ahora puede presionar Ctrl + O para guardar el archivo. Luego, presione Ctrl + X para salir del editor.

Paso 5: Configurar PHP

En este paso, configuraremos el servicio php para procesar las solicitudes entrantes de Nginx. Creará un archivo laravel.ini dentro del directorio php. Este archivo contendrá las configuraciones de PHP. Este es el archivo que había montado mediante bind en /usr/local/etc/php/conf.d/laravel.ini en el contenedor en el Paso 3. Las configuraciones de este archivo anulan el archivo php.ini predeterminado que PHP suele leer cuando se inicia. Introduzca el siguiente comando para crear el directorio php:

Cree y abra el archivo laravel.ini dentro del directorio php introduciendo el siguiente comando:

El archivo php.ini predeterminado tiene un límite de subida establecido en 2M. Como ejemplo, le mostraremos cómo ajustar y establecer las configuraciones de php cambiando el valor del límite de subida permitido, en caso de que desee subir archivos más grandes. Introduzca las siguientes líneas de código dentro del archivo:

Esto establece el límite de subida y puede subir archivos con un tamaño total no superior a 80 MB. Puede añadir otras configuraciones de php dentro del archivo laravel.ini para anular las configuraciones de php predeterminadas. Ahora, guarde y cierre el archivo.

Paso 6: Configurar Nginx

En este paso, configuraremos Nginx para usar el servicio php que definimos anteriormente. Utilizará PHP-FPM como el servidor FastCGI para servir contenido dinámico. El servidor FastCGI es un software que permite a los programas interactivos interactuar con un servidor web.

Como habíamos definido en el archivo docker-compose en el Paso 3, crearemos el archivo de configuración de Nginx app.conf dentro del directorio ~/laravel-web/nginx/conf.d/. Primero, introduzca el siguiente comando para crear el directorio:

A continuación, cree y abra el archivo app.conf usando nano introduciendo el siguiente comando:

Añada el siguiente código de configuración de Nginx al archivo:

Nginx lee archivos de configuración llamados bloques de servidor para saber qué directorio debe servirse a un visitante del sitio web según la URL. Para obtener más información, lea sobre la configuración de bloques de servidor en nuestro tutorial sobre cómo instalar Nginx en Ubuntu 18.04. Las directivas definidas sirven para los siguientes propósitos:

  • listen – Define el puerto en el que el servidor escuchará las solicitudes entrantes, que suele ser el puerto 80.
  • error_log & access_log – Define los archivos para escribir los registros de la aplicación.
  • root – Define la ruta de la raíz web, el directorio que servirá cualquier solicitud realizada al servidor desde Internet.

En el bloque location para php, la directiva fastcgi_pass especifica que el servicio app está escuchando en un socket TCP en el puerto 9000 (que se definió en el Dockerfile). Esto indica al servidor PHP-FPM que escuche a través de la red y no en un socket Unix. Aunque un socket Unix puede tener una ligera ventaja en velocidad sobre un socket TCP, carece de un protocolo de red, por lo que omite la pila de red.

Un socket Unix sería más apropiado para escenarios donde los hosts están ubicados en una sola máquina. Sin embargo, si tiene servicios ejecutándose en diferentes hosts, un socket TCP tiene la ventaja de conectar servicios que están distribuidos. En nuestro caso, el contenedor de la aplicación se está ejecutando en un host diferente al de nuestro contenedor de servidor web. Por lo tanto, un socket TCP es el más apropiado para nuestra configuración.

Ahora puede presionar Ctrl + O para guardar el archivo, luego presione Ctrl + X para salir del editor. Los cambios realizados en el directorio nginx/conf.d/ se reflejarán automáticamente en el contenedor del servidor web gracias al montaje bind que agregó en el archivo Docker Compose en Paso 3.

Paso 7: Configurar MySQL

Después de haber configurado Nginx para que funcione con PHP, ahora podemos configurar MySQL para almacenar y servir datos dinámicos a PHP. Ya habíamos configurado el archivo Docker Compose para instalar las extensiones necesarias para la comunicación entre PHP y MySQL. Crearemos el archivo de configuración de MySQL my.cnf dentro de la carpeta mysql, que se montará mediante bind en /etc/mysql/my.cnf como habíamos definido en la sección del servicio db de Docker Compose en Paso 3.

Los ajustes de configuración y los cambios de MySQL se pueden realizar en el archivo my.cnf cuando lo desee. Deberían reflejarse dentro del contenedor de inmediato. Primero, cree el directorio ingresando el siguiente comando:

A continuación, cree y abra el editor nano ingresando el siguiente comando:

Ingrese el siguiente fragmento de código para habilitar el registro de consultas y especificar la ubicación del archivo de registro de consultas:

Al definir la propiedad general_log como 1, permite los registros generales. La propiedad general_log_file especifica la ubicación del archivo de registros. Presione Ctrl + O para guardar el archivo, luego presione Ctrl + X para salir del editor.

Paso 8: Configurar las variables de entorno de Laravel

Hasta este punto, todos los servicios y ajustes de configuración están completos. Por lo tanto, podríamos implementar nuestros contenedores. Sin embargo, hay un paso importante que debe realizarse antes de que nuestra aplicación web sea realmente utilizable: las variables de entorno. El framework Laravel espera un archivo llamado .env que utiliza para definir su entorno. Por defecto, Laravel viene con el archivo .env.example que puede copiar a .env, para luego modificar las variables con sus datos reales. Ingrese el siguiente comando para copiar el archivo:

Una vez copiado, abra el archivo usando nano para modificarlo:

Aquí hay una captura de pantalla de cómo podría verse el archivo:

screenshot of what the file

En el archivo, el siguiente paso es modificar las variables bajo el bloque DB_CONNECTION tal como las estableció en las configuraciones anteriores que hemos realizado hasta ahora. Actualice de la siguiente manera:

  • DB_HOST es el db contenedor de la base de datos.
  • DB_DATABASE es laravel_web.
  • DB_USERNAME es el nombre de usuario para la base de datos. Elija el nombre que prefiera, pero para los fines de este tutorial, usemos laraveldocker.
  • DB_PASSWORD es una contraseña segura que su usuario anterior utilizará para iniciar sesión en la base de datos, así que elija una contraseña segura. En el Paso 10, crearemos este usuario con la contraseña que elija aquí.

Con los valores actualizados, su DB_CONNECTION ahora debería verse así:

DB_CONNECTION

Guarde y cierre el archivo.

Paso 9: Ejecutar los contenedores de Docker

En esta etapa, todos sus servicios y configuraciones están definidos en el archivo Docker Compose. Solo se requiere un comando para iniciar todos los contenedores, crear los volúmenes, conectar las redes y configurar y compilar su aplicación. Ingrese el siguiente comando en su terminal:

Cuando ejecutas el comando docker-compose up por primera vez, descarga todas las imágenes de Docker necesarias. Si estás configurando la infraestructura en tu computadora local, puede tomar algún tiempo completarse. Una vez que las imágenes se han descargado, Compose crea los contenedores. La bandera -d indica a Docker que ejecute los contenedores en segundo plano. Si el proceso se completa con éxito, deberías poder ver algo como esto en tu terminal:

terminal

Introduce el siguiente comando en tu terminal para listar todos los contenedores en ejecución:

Debería mostrar algo como la captura de pantalla de abajo, con detalles sobre los contenedores app, webserver y db:

screenshot details

  • CONTAINER ID – Un identificador único para cada contenedor.
  • NAMES – El nombre del servicio asociado con cada contenedor, tal como se define en el archivo Docker Compose. (Puedes usar el ID o el nombre del contenedor para acceder a un contenedor).
  • IMAGE – El nombre de la imagen para cada contenedor.
  • STATUS – Muestra información sobre el estado del contenedor (puede estar detenido, en ejecución o reiniciándose).
  • PORTS – Muestra los puertos que un contenedor está exponiendo.

Docker Compose proporciona un comando llamado exec que puedes usar para ejecutar comandos de terminal o acceder a la línea de comandos dentro de un contenedor. Primero queremos ejecutar algunos comandos dentro del contenedor app, que es el contenedor que ejecuta la aplicación Laravel.

Docker proporciona un comando para acceder a la línea de comandos de un contenedor. Su sintaxis es la siguiente: docker-compose exec container_name bash. Para acceder a la línea de comandos del contenedor app, introduce el siguiente comando:

Una vez en la línea de comandos del contenedor, puedes ejecutar algunos comandos de configuración de Laravel Artisan. Introduce el siguiente comando para generar la clave de laravel y guardarla en el archivo .env:

Con la clave de entorno establecida, puedes ejecutar el siguiente comando para almacenar en caché los ajustes de configuración:

Las configuraciones se almacenan en el archivo /var/www/html/bootstrap/cache/config.php dentro del contenedor. Puedes presionar Ctrl + D para salir de la terminal del contenedor.

Para confirmar que la aplicación Laravel se ha desplegado y se está ejecutando, visita la IP pública de tu servidor’ en tu navegador (http://your_server_public_ip). Deberías ver la página de bienvenida para una nueva instalación de Laravel:

laravel screenshot

Step 10: Configure a MySQL user

En este paso, crearemos un usuario de base de datos para la base de datos MySQL laravel_web que especificamos en el archivo docker-compose. Cuando ejecutaste el comando de construcción del contenedor en el Step 9, se instaló MySQL, pero solo se creó una cuenta administrativa root predeterminada, que resulta tener privilegios ilimitados en la base de datos. Para evitar usar el usuario root, crearemos un usuario dedicado, laraveldocker, para usar en la aplicación. Este es el usuario que especificaste en las variables de entorno en el Step 8. Accede a la línea de comandos dentro de la terminal introduciendo el siguiente comando:

Una vez dentro del contenedor, inicia sesión en MySQL introduciendo el siguiente comando:

En la solicitud de contraseña, introduce la contraseña que estableciste en el servicio db en el archivo docker-compose en el Step 2.

Once you have logged into the MySQL prompt, check if you can see the database you specified in the docker-compose file by entering the following SQL command:

Deberías ver la base de datos laravel_web, o cualquier nombre que hayas especificado para tu configuración:

Laravel

A continuación, creamos un usuario y una contraseña para la base de datos laravel_web. Estos deben ser los mismos detalles que especificaste en el archivo .env en el Step 8. Introduce el siguiente comando para crear el usuario y la contraseña, y otorgar todos los privilegios a este usuario:

Para que los cambios surtan efecto de inmediato, introduzca el siguiente comando para vaciar los privilegios:

Con esto se completa la configuración del usuario de MySQL. Salga de la consola de MySQL escribiendo exit y presionando enter. Finalmente, salga del contenedor db presionando Ctrl + D.

Paso 11: Probar la comunicación entre el código de la aplicación Laravel y la base de datos MySQL

Hasta este paso, todo ha funcionado bien. Sin embargo, queremos confirmar que el código de Laravel en el contenedor app puede comunicarse con la base de datos MySQL en el contenedor db. Primero, acceda a la terminal del contenedor app introduciendo el siguiente comando:

A continuación, ejecute el comando migración de Laravel que crea las tablas:

Debería ver el proceso de migración en su terminal a medida que Laravel crea las tablas predeterminadas:

migration process

A continuación, probaremos si podemos acceder a la base de datos desde Laravel. Laravel viene con Tinker de forma predeterminada, lo que le permite interactuar con toda la aplicación desde la línea de comandos, incluyendo el acceso a la base de datos, la ejecución de tareas, ORM Eloquent, y más. Podemos usar Tinker para ver los datos en la tabla de migraciones. Introduzca el siguiente comando para acceder a Tinker:

Una vez en la consola de Tinker, puede listar las tablas creadas por el comando migrate introduciendo lo siguiente:

La siguiente captura de pantalla muestra la salida, que son las tablas actualmente en la base de datos laravel_web:

laravel_web database

Puede recuperar los datos de una tabla especificando el nombre de la tabla. Por ejemplo, puede recuperar los datos de la tabla migrations introduciendo el siguiente comando:

El comando muestra la siguiente salida:

comman output

A partir de la salida anterior, su aplicación Laravel está bien configurada y puede comunicarse con la base de datos. Puede experimentar con más comandos como la creación de modelos, la ejecución de tareas y más. Puede salir de la consola de Tinker presionando Ctrl + D.

Conclusión

En este tutorial, ha desplegado una aplicación Laravel con pila LEMP dentro de un contenedor Docker. Ha probado la aplicación accediendo a la interfaz web y conectándose a la base de datos a través de Laravel Tinker. Ha podido experimentar el poder de Docker Compose. Le permite crear un grupo de contenedores Docker definidos en un solo archivo, que luego se pueden ejecutar con un solo comando.

Si desea familiarizarse aún más con los contenedores, eche un vistazo a nuestro tutorial que le muestra cómo limpiar los recursos de Docker: imágenes, contenedores y volúmenes y a nuestra visión general detallada de la herramienta Kubernetes.

También puede visitar nuestro blog para obtener más información sobre Docker e Integración Continua y Despliegue Continuo.

¡Feliz computación!

author

Pranay Kapgate

Autor · CloudSigma

Preslav Dobrev es diseñador creativo en CloudSigma, centrado en una identidad empresarial coherente mediante el uso de canales de marketing tradicionales e innovadores. Es experto en fusionar la visión artística con el marketing estratégico para crear narrativas de marca impactantes.

Comentarios

Aún no hay comentarios. Sea el primero.