Creando un entorno de desarrollo con Docker

Carlos Compains
7 min readOct 23, 2018

--

Para crear el entorno de desarrollo utilizaré docker-compose para poder levantar todo el entorno en un solo comando. Ese entorno lo podremos levantar tanto en la máquina local como en una máquina remota de desarrollo.

En este fichero será donde se definan las dependencias de sistema de nuestra aplicación. Según como sea nuestra arquitectura podremos usar ese mismo fichero con unas pocas modificaciones para desplegar en produccion.

En este ejemplo vamos a suponer que usamos una aplicación Symfony junto con MySQL y Elasticsearch.

Instalar Docker en Ubuntu 16.04

Usando docker-compose

Es muy probable que nuestro proyecto utilize algún servicio externo como MySql, MariaDb, Redis, Memcached o Elasticsearch. Necesitaremos tener estos servicios disponibles mientras desarrollamos y además de una manera sencilla.

Docker-compose es una herramienta de Docker que nos permite ejecutar y parar contenedores con un solo comando. La configuración necesaria está en un solo fichero (docker-compose.yml) en el cual además de especificar las imágenes a usar para nuestros contenedores, especificaremos todos los detalles necesarios (puertos expuestos, volúmenes, comandos…)

Un ejemplo:

#docker-compose -f docker/dev/docker-compose.yml up
version: '3'
services:
blog:
build: .
ports:
- 81:80
volumes:
- ../../:/var/www
- ../uploads:/media/uploads
command: /usr/sbin/apache2ctl -D FOREGROUND
environment:
- "DATABASE_PASSWORD=12345"
- “DATABASE_USER=mysql_user"
- "DATABASE_HOST=mysql"
- "DATABASE_DB_NAME=site"
mysql:
image: mysql
ports:
- 6603:3306
environment:
MYSQL_ROOT_PASSWORD: "12345"
volumes:
- ../data:/var/lib/mysql

En este ejemplo hemos definido dos servicios (blog y mysql). Docker-compose añadirá a los ficheros /etc/hosts de cada uno de los contenedores dos entradas de manera que en cada uno de los contenedores podremos usar blog y mysql como nombre de host que apuntarán al contenedor correspondiente. Esto es realmente útil ya que lo podremos usar después para, sin tocar nuestra aplicación, desplegarla pasándole un nuevo host para la base de datos, por ejemplo.

En el caso del servicio blog, además le estamos diciendo que tiene que crear la imagen desde el fichero Dockerfile que está en el mismo directorio.

También le decimos que “conecte” el puerto 81 de la máquina donde está alojado el contenedor con el puerto 80 del mismo, es decir estamos exponiendo el puerto http del contenedor al mundo exterior.

Persistencia de ficheros, los volúmenes

En la sección volumes le decimos que monte dos volúmenes en el contenedor. Es decir estamos conectando dos directorios de la máquina con el contenedor. Uno de ellos contiene nuestra aplicación y el otro ficheros subidos a la misma por parte del usuario.

Los cambios que se realizan en un contenedor se pierden cuando se destruye (esto es matizarle ya que hay un comando para guardar esos cambios). Esto quiere decir que por ejemplo si no montamos un volumen para las subidas que haga el usuario, cuando actualicemos el contenedor ,es decir, se pare, borre, descargue la nueva imagen y ejecute uno nuevo, eso ficheros (las imágenes del blog por ejemplo) se perderán.

Lo mismo pasa si, por ejemplo el contenedor ejecuta MySql. Si no montamos un volumen externo al contenedor para los datos, los cambios que se realicen sobre la base de datos, se perderán si actualizamos el contenedor, o si lo eliminamos y volvemos a lanzar.

Ejecutar comandos en Docker

Los contenedores Docker mueren cuando el comando que se les pasa termina. Esto quiere decir que si por ejemplo le pedimos a Docker que ejecute ‘ls -l /‘ Docker levantará el contenedor, listara los ficheros que hay en la raíz del disco y parará el contenedor.

Si nuestro contenedor contiene un servicio que queremos que se ejecute de manera continuada, puede ser necesario (según como esté hecha la imagen sobre la que construye el contenedor) pasarle un comando extra que lance el proceso correspondiente de manera continua. En el caso del servicio blog, el comando se encarga de que Apache se inicie y se quede corriendo a la espera de peticiones.

Definiendo el entorno

En el bloque environment, definimos las variables de entorno que nuestra aplicación necesita para funcionar. En este caso son las variables necesarias para poder conectar a nuestra base de datos (host, user, password y nombre de la base de datos). Definiendo este tipo de variables de esta manera, podemos “portar” nuestro contenedor a cualquier sitio y ejecutarlo contra cualquier base de datos sin tocar el código, solo cambiando estas variables de entorno.

La base de datos

Como comentaba antes, los cambios en el contenedor se pierden cuando se cambia el contenedor. Es decir, si la base de datos corre en un contenedor, o montamos un volumen para que la base de datos guarde allí los ficheros o los perderemos con cada actualización del contenedor.

Usar o no docker para ejecutar la base de datos, es algo bastante opinable. Personalmente no veo mayor problema en hacerlo según el tipo de proyecto y la etapa del mismo.

Para entornos de desarrollo es muy útil. Podemos correr cualquier base de datos que corra en Linux en un contenedor y “trasladar” el proyecto de un Mac a un Windows o Linux y ejecutarlo con pocos cambios (/usr/bin → c://lo-que-sea)

En entornos de producción, también es factible, pero depende del caso de uso. Creo que una de las ventajas de Docker es poder levantar contenedores de forma sencilla y rápida, pero para ello, los contenedores han de ser stateless, pero por definición una base de datos no lo es, por lo que no podemos levantar copias de la base de datos al vuelo para escalar.

Por ello en entornos de producción suelo optar por una configuración mas tradicional de la BBDD.

Lanzando el entorno de desarrollo

Una vez tenemos el fichero listo llega el momento de probarlo.

Para ello lanzamos el comando:

docker-compose up

en el mismo directorio en el que tenemos el fichero docker-compose.yml o si estamos en otra ruta, podemos ejecutar:

docker-compose -f /ruta/a/docker-compose.yml up

El terminal se quedara ocupado por la ejecución de docker-compose e iremos viendo el log del contenedor. Si queremos que el terminal no se bloquee o queremos dejar corriendo el entorno, podemos añadir la opción -d al final del comando.

Si corremos en modo “dettached” (con la opción -d) podremos acceder a log del entorno con:

docker-compose -f /ruta/a/docker-compose.yml logs

o con

docker-compose -f /ruta/a/docker-compose.yml logs -f

si queremos que se vaya actualizando conforme entra nueva información.

Como se puede especificar la ruta al fichero de configuración es fácil tener varios escenarios listos para ser ejecutados cambiando la ruta. Por ejemplo podemos tener en la raíz de nuestro proyecto el directorio “docker” y dentro de el distintos entornos “dev”, “test” y “prod” y en cada uno de ellos tener el correspondiente fichero docker-compose.yml y si es necesario el fichero Dockerfile correspondiente.

Para parar la ejecución del entorno podemos o bien usar el comando stop que solo lo para o bien usar down que lo para y lo borra:

docker-compose -f /ruta/a/docker-compose.yml stop
docker-compose -f /ruta/a/docker-compose.yml down

Si queremos ver los contenedores que están corriendo podemos ejecutar:

docker ps

obteniendo algo como esto:

69b34ec43ed1 mysql “docker-entrypoint.s…" 2 seconds ago Up 3 seconds 0.0.0.0:6603->3306/tcp mysql-container
ee022eb58b03 dev_blog “/usr/sbin/apache2ct…" 2 seconds ago Up 2 seconds 0.0.0.0:81->80/tcp blog_app

Si queremos acceder a un contenedor, para ver el log de nuestra aplicación o investigar un error o cualquier otra cosa podremos hacerlo con:

docker exec -ti ee022eb58b03 bash

Esto nos cambiara el prompt de la consola y estaremos en el contenedor. Para salir basta con escribir exit y pulsar enter.

Para acceder a nuestra aplicación bastará con acceder a http://localhost:81

Si necesitamos acceder a la base de datos, podremos conectarnos usando las credenciales que hemos especificado en docker-compose.yml y conectándonos a localhost usando el puerto 6603 en vez de 3306.

El fichero Dockerfile, las dependencias de nuestra aplicación a nivel sistema operativo

El fichero Dockerfile será el que le diga a Docker como ha de crear la imagen con la que luego se levantarán los contenedores que ejecutarán nuestra aplicación.

Para mas información sobre la sintaxis de estos ficheros aqui

FROM ubuntu:xenialENV DEBIAN_FRONTEND noninteractive# Repositorio para poder instalar php7.1
RUN apt-get update -y && apt-get install -y software-properties-common python3-software-properties && \
LC_ALL=C.UTF-8 add-apt-repository -y ppa:ondrej/php && \
apt-get update -y
#Fijamos la zona horaria a nivel contendor.
ENV TZ=Europe/Madrid
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# Actualización de sistema e instalación de paquetes
RUN apt-get install -y \
git \
imagemagick \
apache2 \
jpegoptim \
php7.1 \
acl \
libapache2-mod-php7.1 \
php7.1-curl \
php7.1-xml \
php7.1-mbstring \
nodejs \
unzip \
npm \
php7.1-gd \
php7.1-intl \
php7.1-xsl \
php7.1-mysql
# Copia de los ficheros de configuración de apache y php
COPY apache/site.conf /etc/apache2/sites-available/site.conf
COPY php/php.ini /etc/php/7.1/apache2/php.ini
# Habilitar el sitio en apache y deshabilitar el que viene por defecto
RUN a2ensite site.conf && \
a2dissite 000-default.conf
# Habilitar los modulos de apache necesarios
RUN a2enmod php7.1 && \
a2enmod rewrite && \
a2enmod actions && \
a2enmod deflate && \
a2enmod expires && \
a2enmod headers && \
a2enmod actions && \
a2enmod proxy
# Instalar los paquete npm necesarios para la aplicación
RUN npm install -g less && \
npm install uglify-js -g && \
npm install uglifycss -g
# Creación de los directorios necesarios para symfony
RUN mkdir -p /var/www/app/cache && \
mkdir -p /var/www/app/logs && \
mkdir -p /media/tmp && \
mkdir -p /media/uploads
# Enlace simbolico de nodejs para Symfony
RUN ln -s /usr/bin/nodejs /usr/bin/node
# Permisos en los directorios
RUN chmod -R 777 /media/tmp && \
chmod -R 777 /media/uploads
RUN chown -R www-data:www-data /media/tmp && \
chown -R www-data:www-data /media/uploads

Este fichero Dockerfile genera una imagen basada en ubuntu con apache, Php 7.1 y unos cuantos módulos que usa este blog, así como nodejs y dos módulos que usa Assetic para gestionar css y js.

Durante el proceso de construcción se copian los ficheros de configuración de Apache y Php. Si necesitásemos cambiar constantemente estos ficheros para hacer pruebas con ellos, sería más recomendable utilizar volúmenes para ellos (se puede tanto copiar el directorio que los contiene como ficheros concretos) para no tener que hace el proceso de build completo con cada cambio, ya que puede ser costoso en tiempo. Este fichero, según la máquina donde se ejecute y la conexión que tenga puede tardar hasta 50 minutos en completarse),

Conclusiones

Utilizando Docker y docker-compose es sencillo crear entornos que son fácilmente portables a otros desarrolladores y que ademas son independientes (en gran medida) del sistema operativo sobre el que ejecutemos Docker.

Manteniendo de manera correcta las distintas versiones (si las hay) del fichero Dockerfile nos aseguramos de que nuestra aplicación corre exactamente en las mismas condiciones.

--

--

Responses (1)