Hace unos meses publiqué un pequeño tutorial sobre el despliegue de aplicaciones Django, en el que mencioné Docker, pero no expliqué ni su funcionamiento ni sus ventajas, principalmente porque pensaba escribir un tutorial que por falta de tiempo se quedó en el tintero. Desde entonces han aparecido algunos buenos manuales, así que en su lugar he escrito este tutorial sobre como desplegar django usando Docker.

El tutorial asume que el lector posee conocimientos sobre la configuración de nginx y gunicorn, así como del funcionamiento de docker. Si no es así recomiendo leer mi tutorial despliegue de aplicaciones Django, y algún tutorial de docker, antes de continuar.

Introducción

En esencia el proceso de crear una imagen Docker es el mismo que hay que seguir para configurar cualquier servidor manualmente, cada uno de los pasos en el proceso se plasma en un archivo Dockerfile que es usado por Docker para construir la imagen. Por ejemplo para un contenedor de una aplicación django, será necesario:

  • Copiar la aplicación django al contenedor, e instalar sus dependencias.
  • Instalar y configurar gunicorn.
  • Instalar y configurar nginx.
  • Instalar supervisor y configurarlo para que arranque nginx y gunicorn.

Para conseguir todo esto, además del archivo Dockerfile, necesitamos los archivos de configuración de nginx, gunicorn, supervisor, y la aplicación django que se copiarán a la imagen. Todos estos archivos del proyecto deben estar en el mismo directorio que Dockerfile, por ejemplo la estructura usada en este tutorial es:

Docker/
|--Dockerfile
|--gunicorn-config.py
|--nginx-default
|--supervisor.conf
|--django_app/
|  |--manage.py
|  |--requirements.txt
|  |--static/
|  |--app1/
|  |  |--models.py
|  |  |--....
|  |--app2/
|  |  |--....

Descargar

El Dockerfile en este tutorial asume que la aplicación django esta en el subdirectorio django_app, que a su vez contiene el archivo requirements.txt con las dependencias del mismo, y el directorio static con todos los archivos estáticos.

Dockerfile

Toda la magia en la creación de una imagen está en el archivo Dockerfile, su sintaxis es muy clara:

FROM ubuntu:14.04
MAINTAINER secnot <secnot@secnot.com>

# Actualizacion de los 'sources' a la ultima version
RUN apt-get update

# Instalar los paquetes del sistema necesarios para python
RUN apt-get install -qy python \
                        python-dev \
                        python-pip \
                        python-setuptools \
                        build-essential

# Instalar algunas utilidades extras (opcional)
RUN apt-get install -qy vim \
                        wget \
                        net-tools \
                        git

# Instalamos resto aplicaciones
RUN apt-get install -qy nginx \
                        supervisor

###############################
#
#        Nginx
#
###############################

# Copiar la configuracion de nginx de la aplicion
ADD nginx-default /etc/nginx/sites-available/default

# Desactiva el modo demonio para arrancar el proceso con supervisor
RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf

# Cambiar los permisos de nginx para poder ejecutar nginx como www-data
RUN chown -R www-data:www-data /var/lib/nginx

# Permitir el acceso al puerto 80 del contenedor
EXPOSE 80


##################################
#
#        Gunicorn y Django
#
################################

# Copiar aplicacion del subdirectorio django_app/ al directorio
# /django_app en el contenedor
ADD django_app /django_app
RUN chown -R www-data:www-data /django_app

# Si la aplicacion tiene dependencias de paquetes del del sistema
# este es un buen sitio para instalarlas, por ejemplo para PIL:
# RUN apt-get install -qy python-dev libjpeg-dev zlib1g-dev

# Usamos requirements.txt para instalar las dependencias de la
# aplicacion.
RUN pip install -r /django_app/requirements.txt

# Tambien se pueden instalar individualmente, por ejemplo:
# RUN pip install Django
# RUN pip install bleach
# ...

# Una buena medida de seguridad es alamacenar claves usuarios y
# otras credenciales de seguridad en variables de entorno.
# Se importan desde settings.py con:
#       PAYPAL_CLIENT_ID     = os.environ['PAYPAL_CLIENT_ID']
#       PAYPAL_CLIENT_SECRET = os.environ['PAYPAL_CLIENT_SECRET']
ENV PAYPAL_CLIENT_ID sdfasFASDRwefasFqasdfAsdfAsdFAsdfsDFaSDfWERtSDFg
ENV PAYPAL_CLIENT_SECRET ASAsdfarasDFaRasdFaSsdfghJdfGHDGsdTRSDfGErtAFSD

# Como precaucion se instala gunicorn, aunque deberia estar en
# requirements.txt
RUN pip install gunicorn

# Por ultimo se copia la configuracion de gunicorn.
ADD gunicorn-config.py /etc/gunicorn/config.py


#############################
#
#        Supervisor
#
############################

# Copiar la configuracion
ADD supervisor.conf /etc/supervisor/conf.d/django_app.conf

# Instalamos supervisor-stdout que permite que los logs, sean impresos
# en stdout. (ver supervisor.conf)
RUN pip install supervisor-stdout

# Establecer el directorio de trabajo
WORKDIR /django_app

# Comando por defecto que se ejecutara al arranque del contenedor,
# supervisor se encarga de gestionar nginx y gunicorn.
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"]

Supervisor y Logging

El archivo supervisor.conf, contiene la configuración necesaria para que supervisord arranque nginx y gunicorn, y los reinicie en caso de que mueran:

[program:gunicorn]
command = /usr/local/bin/gunicorn -c /etc/gunicorn/config.py yourapp.wsgi:application
directory = /django_app
user = www-data
autostart = True
autorestart = True

[program:nginx]
command = /usr/sbin/nginx
autostart = True
autorestart = True

Pero esa no es la única función de supervisor, sino que también se encarga de guardar logs de las salidas stdout y stderr de cada programa que gestiona. Con django se puede aprovechar esta característica para que supervisor se encargue de gestionar los logs, solo es necesario imprimirlos en stdout en lugar de guardarlos en un archivo, editando settings.py de tu aplicación:

# settings.py
# .....
import sys

LOGGING = {
    'version': 1,
    'dissable_existing_loggers': False,

    'formatters': {
        'verbose': {
            'format': "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s",
            'datefmt': "%Y-%m-%d %H:%M:%S",
        },
        'simple': {
            'format': "%(levelname)s %(message)s",
        },
    },

    'handlers': {
        'console': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'stream': sys.stdout,
            'formatter': 'simple',
        },
        'mail_admins': {
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler',
            'include_html': True,
        },
    },

    'loggers': {
        '': {
            'handlers': ['mail_admins', 'console'],
            'level': 'DEBUG',
        }
    },
}

El manejador StreamHandler redirige todos los logs de nivel INFO, o superior a la salida estándar, cuando supervisor recibe estos logs tiene dos opciones:

  • Almacenar los logs en un archivo, que a su vez puede o no estar montado mediante un volumen desde el host, para que los datos sean persistente:

    [program:gunicorn]
    command = /usr/local/bin/gunicorn -c /etc/gunicorn/config.py yourapp.wsgi:application
    directory = /django_app
    user = www-data
    autostart = True
    autorestart = True
    
    redirect_stderr=true
    stdout_logfile=/var/log/django.log
    

    Para usar un volumen, se debe iniciar el contenedor con la opción -v:

    $ docker run -v /var/log/django.log:/var/log/django.log -p 80:80 yourapp:nginx
    
  • Redirigir los logs a stdout para que sean gestionados por el host que inicio el contenedor. Para esto es necesario usar el manejador de eventos supervisor-stdout. Y la configuración de supervisor seria:

    [supervisord]
    nodaemon = true
    
    [program:gunicorn]
    command = /usr/local/bin/gunicorn -c /etc/gunicorn/config.py testsite.wsgi:application
    directory = /django_app
    user = www-data
    autostart = True
    autorestart = True
    stdout_events_enabled = true
    stderr_events_enabled = true
    
    [program:nginx]
    command = /usr/sbin/nginx
    autostart = True
    autorestart = True
    stdout_events_enabled = true
    stderr_events_enabled = true
    
    
    [eventlistener:stdout]
    command = supervisor_stdout
    buffer_size = 100
    events = PROCESS_LOG
    result_handler = supervisor_stdout:event_handler
    

    Para arrancar el contenedor, hay que añadir la opción -a stdout para que imprima la salida en stdout:

    $ sudo docker run --rm -a stdout -p 80:80 yourapp:nginx
    

    En el host se puede configurar supervisor para que arranque el contenedor, y al tiempo guardar los logs

Seguridad

Los contenedores docker NO son seguros, a pesar de lo que hayas leído es posible salir de un contenedor. Por diseño los contenedores comparten el mismo kernel que el host, y pueden hacer llamadas de sistema al mismo, de manera que cualquier vulnerabilidad del interfaz del kernel, es explotable desde un contenedor. La mejor política es tratar docker como una herramienta para desplegar aplicaciones de forma sencilla, que además proporciona una capa extra de seguridad. Dicho esto, es posible mejorar la seguridad con medidas sencillas:

  • NO ejecutes como root los procesos dentro del contenedor, usa un usuario sin privilegios:

    $ docker run -u=www-data yourapp:nginx
    

    con -u hay que indicar un usuario o uid existente en el contenedor.

  • Limita la memoria disponible para los procesos del contenedor con la opción -m, por ejemplo 200MB:

    $ docker run -m=200m yourapp:nginx
    
  • Limita el uso de cpu, permitiendo la ejecución únicamente en los nucleos/cpus especificados con --cpuset:

    $ docker run --cpuset=0,1 yourapp:nginx
    
  • NO uses volúmenes para montar el sistema de archivos del host en el contenedor, es cómodo pero es difícil de configurar correctamente para que sea seguro. Si no tiene mas remedio que usarlos, móntalos en modo lectura:

    $ docker run -v /host/static:/container/static:ro
    
  • Utiliza versiones reciente del kernel, como he comentado anteriormente todas las vulnerabilidades del kernel son explotables desde el contenedor, instala versiones recientes y esta atento a la aparición de nuevos exploits.

  • Usa una máquina virtual en la que ejecutar docker, de esta manera tienes lo mejor de los dos mundos, la seguridad de una VM, con la facilidad para desplegar aplicaciones de docker.

Comandos útiles

Una vez la aplicación esté correctamente configurada, puedes construir el contenedor ejecutando el siguiente comando desde el directorio Docker:

$ sudo docker build --rm:True -t yourapp:nginx .

y deberia aparecer al listar las imágenes disponibles:

$ sudo docker.io images

REPOSITORY     TAG     IMAGE ID        CREATED             VIRTUAL SIZE
yourapp        nginx   fb3367f96602    4 minutes ago       544.4 MB
ubuntu         14.04   826633116fdc    5 days ago          194.2 MB

y por ultimo se ejecuta el contenedor con:

$ sudo docker run --rm -a stdout -p 80:80 yourapp:nginx

El parámetro -p 80:80 le indica a docker que publique el puerto 80 del contenedor en el puerto 80 local, de forma que el contenedor sea accesible desde el exterior. -rm indica que el contenedor debe ser eliminado una vez finalice.

Por defecto para crear y ejecutar contenedores es necesario, tener privilegios de root, esto es debido a que el demonio docker se ejecuta como root, y solo es posible comunicarse con el a través de de un socket unix, propiedad del usuario root y el grupo docker.

Para que un usuario sea capaz de manejar contenedores, solo es necesario añadirlo al grupo docker, y reiniciar el demonio:

$ sudo usermod -a -G docker gowen
$ sudo service docker restart

Ver comentarios


Si estás desarrollando una aplicación web, algún día tendrás que dar el paso y hacerla pública. Inicialmente puedes usar un servicio como Heroku pero si tienes éxito, no te quedará mas remedio que gestionar tus propios servidores o VPS.

Por fortuna desplegar una aplicación django usando nginx y gunicorn, es más sencillo de lo que podría parecer. En esta pequeña guia trato de describir el proceso paso a paso:

1 - Crear un usuario para la aplicación django

El primer paso es crear un usuario con el que ejecutar la aplicación Django, esto nos permite organizar con facilidad un servidor donde estemos ejecutando varias aplicaciones, proporciona separación de privilegios, y limita el posible daño que pueda hacerse al sistema si la aplicación es comprometida.

Creamos un grupo al que pertenecerán todos los usuarios de las aplicaciones django, y un usuario al que le asignamos el nombre de la aplicación.

$ sudo addgroup --system webapps
$ sudo adduser  --system --ingroup webapps --home /webapps/appname appname

Al usar en modificador --system, al usuario se le asigna /bin/false como shell, y no tiene clave, por lo que no puede hacer login. Aunque esto es una buena medida de seguridad, es muy incomodo mientras se está configurando el sistema, así que asignamos un shell temporalmente:

$ sudo chsh -s /bin/bash appname

Esto nos permite usar sudo su appname para seguir con la instalación como el nuevo usuario.

2 - Instalar y configurar virtualenv

Virtualenv es la herramienta que nos permite aislar los paquetes requeridos por las aplicaciones, de manera que si dos aplicaciones necesitan paquetes que están en conflicto, no interfieran la una con la otra como ocurriría si instalásemos todos los paquetes directamente en el sistema. Instalamos virtualenv con:

$ sudo apt-get install python-virtualenv python-pip

Una vez instalado, hacemos login con el usuario que hemos creado, y dentro de su directorio usamos virtualenv para generar un nuevo entorno virtual:

$ sudo su appname
$ cd
$ mkdir virtualenvs
$ virtualenv --no-site-packages virtualenvs/app_env
$ source virtualenvs/app_env/bin/activate

Dentro del entorno, hay que instalar todos los paquetes que sean necesarios para la aplicación, puedes hacerlo uno a uno, o en bloque si tienes el archivo requirements.txt:

(app_env)$ pip install django
(app_env)$ pip install django-countries
(app_env)$ pip install django-mptt
(app_env)$ ....
(app_env)$ pip install -r requirements.txt

Si quieres profundizar en el funcionamiento de virtualenv sigue este tutorial

3 - Gunicorn + Supervisor

Gunicorn es el servidor WSGI que se encarga de servir la aplicación, pero necesita un programa que lo inicie al arranque, y lo monitorice para reiniciarlo si hay algún problema. La mejor solución es usar el gestor de procesos como supervisor. La instalación es sencilla:

$ sudo apt-get install supervisor

Para instalar gunicorn el método más sencillo es hacerlo dentro del entorno virtual de tu aplicación django.

$ source virtualenvs/app_env/bin/activate
(app_env)$ pip install gunicorn

Una vez todo está instalado, creamos un archivo de configuración para gunicorn gunicorn_conf.py en el directorio HOME del usuario, en el que indicamos la dirección y puerto en los que estará escuchando gunicorn:

# gunicorn_conf.py
workers = 3
bind = '127.0.0.1:9000'

A partir de aquí ya podemos salir del entorno virtual de la aplicación:

(app_env)$ deactivate

El siguiente paso es crear el archivo de configuración de supervisor en /etc/supervisor/conf.d/appname.conf:

[program:appname]
command=/webapps/appname/django_app/run_gunicorn.sh
directory = /webapps/appname/django_app/
user=appname
autostart=true
autorestart=true
priority=991
stopsignal=KILL

Tras esto creamos en el directorio de la aplicación el script run_gunicorn.sh que etablece el entorno virtualenv para la aplicación y despuer ejecuta gunicorn:

#!/bin/bash
source /webapp/appname/virtualenvs/app_env/bin/activate
exec /webapp/appname/virtualenvs/app_env/bin/gunicorn -c /webapps/appname/gunicorn_conf.py django_app.wsgi:application

Una vez configurado indicamos a supervisor que debe iniciar el nuevo servicio:

$ sudo supervisorctl start reread
appname: available
$ sudo supervisorctl update
appname: added process group
$ sudo supervisorctl status appname
appname                          RUNNING    pid 21710, uptime 0:00:07

Para detener e iniciar la aplicación podemos usar:

$ sudo supervisorctl stop appname
appname: stopped
$ sudo supervisorctl start appname
appname: started

Llegado a este punto si todo funciona correctamente, ya podemos desactivar el shell del usuario creado para la aplicación.

$ sudo -s /bin/false appname

Si prefieres que sea posible hacer login con el usuario, es recomendable que le asignes una clave.

4 - Nginx

Usamos Nginx para hacer de pasarela entre los clientes y gunicorn, y para servir los archivos estáticos de la aplicación. Para instalarlo:

$ sudo apt-get install nginx

Para configurar nginx, hay que crear un archivo /etc/ngix/sites-available/, en el especificaremos donde se debe conectar a gunicorn en la sección upstream, y la ruta a los archivos estáticos de tu aplicación en la sección server:

upstream django_app_server {
    # Dirección en la que está escuchando gunicorn
    server 127.0.0.1:9000 fail_timeout=0;
}

server {
    # listen 80 default deferred; # for Linux
    # listen 80 default accept_filter=httpready; # for FreeBSD
    listen 80 default;

    client_max_body_size 4G;
    server_name www.dominio.com;

    # ~2 seconds is often enough for most folks to parse HTML/CSS and
    # retrieve needed images/icons/frames, connections are cheap in
    # nginx so increasing this is generally safe...
    keepalive_timeout 5;

    # Ruta a tus archivos estaticos.
    location /static/ {
      alias /webapps/appname/django_app/static/;
      autoindex on;
    }


    location / {
      # an HTTP header important enough to have its own Wikipedia entry:
      #   http://en.wikipedia.org/wiki/X-Forwarded-For
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

      # enable this if and only if you use HTTPS, this helps Rack
      # set the proper protocol for doing redirects:
      # proxy_set_header X-Forwarded-Proto https;

      # pass the Host: header from the client right along so redirects
      # can be set properly within the Rack application
      proxy_set_header Host $http_host;

      # we don't want nginx trying to do something clever with
      # redirects, we set the Host: header above already.
      proxy_redirect off;

      # set "proxy_buffering off" *only* for Rainbows! when doing
      # Comet/long-poll stuff.  It's also safe to set if you're
      # using only serving fast clients with Unicorn + nginx.
      # Otherwise you _want_ nginx to buffer responses to slow
      # clients, really.
      # proxy_buffering off;

      # Try to serve static files from nginx, no point in making an
      # *application* server like Unicorn/Rainbows! serve static files.
      proxy_pass http://django_app_server;

    }
}

Descargar

Una vez configurado, hay que enlazar el archivo que hemos creado en /etc/nginx/sites-available/ a /etc/nginx/sites-enabled/, de esta manera nginx empezará a usar la configuración:

$ sudo ln -s /etc/nginx/sites-available/midominio /etc/nginx/sites-enabled/midominio

Por último reiniciamos nginx:

$ sudo service nginx restart

Nota: Los autores de gunicorn tiene un ejemplo más completo de un archivo de configuración nginx.conf

Alternativas

Por supuesto esta no es la única opción para desplegar django, ni es la mejor para todas las situaciones. Si estas administrando múltiples aplicaciones, y/o múltiples servidores quizás Docker o Dokku se ajuste mejor a tus necesidades. Y como comenté al inicio Heroku es una buena alternativa para páginas con poco tráfico.

Ver comentarios