Este tutorial se desarrolló en marzo de 2016 con fines informativos. No se actualizará para futuras versiones de la tecnología o para dependencias utilizadas en él.

En este tutorial, veremos cómo desplegar un full stack (una aplicación web Django con PostgreSQL y Redis) utilizando Docker-Compose.

Primero, asegúrate de tener tu configuración de Docker preparada, o sigue las instrucciones de esta documentación, dependiendo de tu distribución. En este caso, supondremos que la máquina host y de desarrollo es un Debian 8.

También necesitarás Docker Compose.

Desplegaremos un total de 5 contenedores Docker:

Aquí puedes ver un esquema de la plataforma Docker final:

docker_compose

También crearemos una copia de seguridad de la base de datos automáticamente por las noches.

Para empezar, creamos un nuevo repositorio de git donde irá todo (esto es para facilitarte el proceso, no es un paso obligatorio). Coloquemos dentro los siguientes directorios:

Lo básico

Primero, crearemos un archivo env y el directorio root del proyecto que contendrá las variables de entorno compartidas por muchos sistemas y scripts:

DB_NAME=myproject_web
DB_USER=myproject_web
DB_PASS=shoov3Phezaimahsh7eb2Tii4ohkah8k
DB_SERVICE=postgres
DB_PORT=5432

Entonces, creamos el archivo docker-compose.yml con el siguiente contenido. Déjame que te explique la definición de cada host:

web:
  restart: always
  build: ./web/
  expose:
    - "8000"
  links:
    - postgres:postgres
    - redis:redis
  env_file: env
  volumes:
    - ./web:/data/web
  command: /usr/bin/gunicorn mydjango.wsgi:application -w 2 -b :8000
nginx:
  restart: always
  build: ./nginx/
  ports:
    - "80:80"
  volumes_from:
    - web
  links:
    - web:web

postgres:
  restart: always
  image: postgres:latest
  volumes_from:
    - data
  volumes:
    - ./postgres/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
    - ./backups/postgresql:/backup
  env_file:
    - env
  expose:
    - "5432"
redis:
  restart: always
  image: redis:latest
  expose:
    - "6379"

data:
  restart: always
  image: alpine
  volumes:
    - /var/lib/postgresql
  command: "true"

• Creamos un contenedor Redis estándar, usando la última versión de la imagen oficial y exponiendo el puerto Redis.
• En el contenedor de datos, usamos true (no hace nada, solamente mantenemos el contenedor en ejecución) como un comando, ya que solo queremos que este contenedor contenga el espacio de tablas de PostgreSQL.
• Usar un contenedor de datos es lo recomendado cuando queremos administrar la persistencia de los datos. Al usarlo, no nos arriesgamos a que haya ninguna eliminación por accidente durante una actualización del contenedor PostgreSQL, por ejemplo (que eliminaría todos los datos de tu contenedor).

El contenedor de PostgreSQL

Primero, configuraremos el contenedor de PostgreSQL para que se inicie solo. La imagen de Postgre carga todos los scripts por defecto en el directorio /docker-entrypoint-initdb.d. Vamos a crear un script simple que cree un usuario y una base de datos usando la información del archivo env:

> cat postgres/docker-entrypoint-initdb.d/myproject_web.sh
#!/bin/env bash
psql -U postgres -c "CREATE USER $DB_USER PASSWORD '$DB_PASS'"
psql -U postgres -c "CREATE DATABASE $DB_NAME OWNER $DB_USER"
> chmod a+rx postgres/docker-entrypoint-initdb.d/myproject_web.sh

El contenedor Reverse proxy

Configuramos el contenedor nginx, la forma recomendada de hacerlo cuando se usa este tutum/nginx es a través de un Dockerfile. Vamos a crear uno simple:

> cat nginx/Dockerfile
FROM tutum/nginx

RUN rm /etc/nginx/sites-enabled/default
ADD sites-enabled/ /etc/nginx/sites-enabled

Solamente tenemos que crear un archivo en sites-enabled/ para que sirva los archivos estáticos y que redirija el resto a la aplicación:

> cat nginx/sites-enabled/django
server {

    listen 80;
    server_name not.configured.example.com;
    charset utf-8;

    location /static {
        alias /data/web/mydjango/static;
    }

    location / {
        proxy_pass http://web:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

}

El contenedor de la aplicación web

La manera fácil es simplemente usar una imagen oficial de Python que está basada en Debian, aunque la imagen resultante será bastante grande (la mía era de unos 900MB).

Queremos crear una imagen front muy ligera, para poder actualizarla fácil y rápidamente cuando cambiemos el código y queremos reducir al máximo la superficie de ataque desde el exterior. Para ello, basaremos nuestra imagen en Alpine Linux, que se especializa en esto y es muy común en el mundo Docker.

Entonces, creamos nuestro Alpine personalizado para la aplicación de Django, ejecutando primero una sesión interactiva para crear el proyecto. Y lo siguiente será crear el Dockerfile.

Pero primero, rellena el web/requirements.txt con todos los módulos de Python que necesitamos:

> cat web/requirements.txt
Django==1.8.5
redis==2.10.3
django-redis==4.3.0
gunicorn==19.3.0
psycopg2==2.6

Luego, podemos empezar:

> docker run -ti --rm -v /root/myproject/web/:/data/web alpine:latest sh
/ # cd data/web/
/data/web # apk add --update python3 python3-dev postgresql-client postgresql-dev build-base gettext vim
/data/web # pip3 install --upgrade pip
/data/web # pip3 install -r requirements.txt
/data/web # django-admin startproject mydjango
/data/web # mkdir mydjango/static

Crearemos una instancia para crear nuestra aplicación de Django. Ten en cuenta que lo hacemos para no tener que instalar Python y sus dependencias en nuestro sistema host. Empezaremos una instancia Alpine, e interactivamente:

> docker run -ti --rm -v /root/myproject/web/:/data/web alpine:latest sh
/ # cd data/web/
/data/web # apk add --update python3 python3-dev postgresql-client postgresql-dev build-base gettext vim
/data/web # pip3 install --upgrade pip
/data/web # pip3 install -r requirements.txt
/data/web # django-admin startproject mydjango
/data/web # mkdir mydjango/static

Poblemos el archivo de configuración (mydjango/settings.py) con parámetros compatibles con la información que Docker proporciona en los contenedores. Elimina todo el contenido entre la parte de “DATABASE” y la de “INTERNATIONALIZATION” y reemplázalo con esto:

if 'DB_NAME' in os.environ:
    # Running the Docker image
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.postgresql_psycopg2',
            'NAME': os.environ['DB_NAME'],
            'USER': os.environ['DB_USER'],
            'PASSWORD': os.environ['DB_PASS'],
            'HOST': os.environ['DB_SERVICE'],
            'PORT': os.environ['DB_PORT']
        }
    }
else:
    # Building the Docker image
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
        }
    }

CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://redis:6379/0",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        }
    }
}

SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"

Una vez esté hecho, deja el contenedor en exit (la marca –rm al crear la instancia lo destruirá al salir, la aplicación se guarda en el volumen montado).

Aquí cargamos la base de datos de Docker si encontramos la variable de entorno proporcionada por el archivo env, y usamos toda esa información para conectarnos. Si no se puede, es porque estamos creando la imagen, y no queremos que esto bloquee algunos comandos (como si quisieras compilar la traducción de gettext de tu sitio web).

Para el contenedor de Redis, solo apuntamos a las DNS de Redis que estará presente una vez lo hayamos desplegado utilizando Docker-Compose.

Ahora que tenemos nuestra pequeña aplicación de Django, pongamos todo lo que necesitamos en el Dockerfile del contenedor web:

FROM alpine

# Initialize
RUN mkdir -p /data/web
WORKDIR /data/web
COPY requirements.txt /data/web/

# Setup
RUN apk update
RUN apk upgrade
RUN apk add --update python3 python3-dev postgresql-client postgresql-dev build-base gettext
RUN pip3 install --upgrade pip
RUN pip3 install -r requirements.txt

# Clean
RUN apk del -r python3-dev postgresql

# Prepare
COPY . /data/web/
RUN mkdir -p mydjango/static/admin

Como puedes ver, aquí también limpiamos los paquetes que no necesitaremos después de instalar los módulos de Python (por ejemplo, para compilar el módulo de PostgreSQL).

Deja que surja la magia

Tómate un café (dependiendo de tu velocidad de conexión) y móntalo todo.

> docker-compose build
> docker-compose up -d

Así puedes comprobar tus contenedores de la siguiente manera:

> docker-compose ps
        Name                      Command               State         Ports
----------------------------------------------------------------------------------
myproject_data_1       true                             Up
myproject_nginx_1      /usr/sbin/nginx                  Up      0.0.0.0:80->80/tcp
myproject_postgres_1   /docker-entrypoint.sh postgres   Up      5432/tcp
myproject_redis_1      /entrypoint.sh redis-server      Up      6379/tcp
myproject_web_1        /usr/bin/gunicorn mydjango ...   Up      8000/tcp

> docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                          PORTS                NAMES
d7162329302d        myproject_nginx     "/usr/sbin/nginx"        2 minutes ago       Up 2 minutes                    0.0.0.0:80->80/tcp   myproject_nginx_1
402c2ca47789        myproject_web       "/usr/bin/gunicorn my"   2 minutes ago       Up 2 minutes                    8000/tcp             myproject_web_1
2c92e1fa0021        postgres:latest     "/docker-entrypoint.s"   8 minutes ago       Up 2 minutes                    5432/tcp             myproject_postgres_1
ad58f3138339        alpine              "true"                   9 minutes ago       Restarting (0) 58 seconds ago                        myproject_data_1
29ece917fcbb        redis:latest        "/entrypoint.sh redis"   9 minutes ago       Up 2 minutes                    6379/tcp             myproject_redis_1

Es normal que los datos del contenedor se “reinicien”, ya que no tiene un proceso persistente.

Si todo está bien, ve a tu IP pública y allí deberías ver la página de Django por defecto. (Eso solo debería funcionar si se puede conectar con éxito a la base de datos y cargar el motor de caché):

django

Las copias de seguridad

Si todo fue bien, podemos configurar copias de seguridad para la base de datos de PostgreSQL. Aquí simplemente creamos un pequeño script para que lo hiciera cada noche y lo subiera a S3 (en un bucket con la versión habilitada y una política de ciclo de vida)

/scripts/do_backup.py

import os
import socket
import boto3

BUCKET = "myproject-backups"
S3_DIRECTORY = socket.gethostname()
DB_BACKUP_FILE = "myproject_postgres_1.sql"
DB_BACKUP_PATH = "/tmp/{filename}".format(filename=DB_BACKUP_FILE)
DB_S3_DIRECTORY = "{directory}/postgresql".format(directory=S3_DIRECTORY)

s3 = boto3.resource('s3')

os.system("docker exec -u postgres myproject_postgres_1 pg_dumpall > {path}".format(path=DB_BACKUP_PATH))

backup = open(DB_BACKUP_PATH, "rb")
s3.Object(BUCKET, "{directory}/{filename}".format(directory=DB_S3_DIRECTORY, filename=DB_BACKUP_FILE)).put(Body=backup)

No te olvides de instalar el módulo boto3 PIP y el cli de AWS, así como de configurar tus credenciales con aws configure

Entonces, simplemente configura una tarea de cron y planifícala cuando quieras.

Fin

Ahora cuentas con un entorno de Docker-Compose totalmente operativo 🙂 Cuando hagas una modificación, simplemente tienes que ejecutar un nuevo build de docker-compose; docker-compose up -d y todo se actualizará automáticamente.

 

TAGS: debian, django, docker, docker compose, Labs, PostgreSQL

speech-bubble-13-icon Created with Sketch.
Comentarios
Jesus Valdez | diciembre 21, 2017 12:19 am

I can describe this tutorial with one word: WOW, it was really easy to implement and is the only tutorial that shows exactly what I wanted, thanks a lot!

I am just a bit confused about the ports used on the docker-compose images, I am not sure how are they picked and for what are they used, besides the difference between expose and ports, for example here:

expose:
– «5432»

ports:
– «80:80»

Basically I would like to know what are these numbers, where are they used and why those numbers, for example on Nginx it has to be 80:80 or can be another like 12345:23516?

I have read the Docker official documentation but I don´t quite understand, could you please tell me a little about it. Thanks a lot again

Reply
Marc Egea i Sala | enero 15, 2018 8:29 am

Hi Jesus,

I guess you are familiar with application ports: each application is listening to a port number, so a single host can have multiple services, each one mapped to a port.

The default port for http is the 80, or 443 if it’s https. Other services have default ports. PostgreSQL, for example, uses 5432.

Most of them can be changed configuring the services (in this example, you can see a «listen 80;» in the nginx configuration) but I won’t recommend doing it without a good reason.

Now, what does «ports» do? It maps hosts ports to container hosts. In the example («80:80») this means that if you do a petition to the host’s (the machine running docker, usually your computer or ‘localhost’) port 80, this petition will be directly passed to the port 80 of the container.

The format is HOST:CONTAINER. This means you don’t need to use the same port. If you set «80:3000» all the request to localhost’s port 80 will be send to the container’s port 3000.

The expose command is simpler: it allows linked containers to communicate with this container using this port. It’s mean for internal communication between containers and not directly from the host.

There’s more (and better explained) information in the docker-compose documentation:
https://docs.docker.com/compose/compose-file/#ports
https://docs.docker.com/compose/compose-file/#expose

Reply
Jesus Valdez | enero 27, 2018 10:21 pm

thanks a lot for the explanation, then if I want to get on nginx image what comes from web using let´s say port 8080, shouldn´t I set expose: 8080 on web and on nginx do something like
ports:
8080:80

from what I see on your code you are using 8000 on wsgi, then exposing that same 8000, but then on nginx you are setting
ports:
80:80

shouldn´t it be 8000:80 so it will link to web and then publishing it on 80?

Reply
Marc Egea i Sala | enero 30, 2018 4:39 pm

Hi Jesus,

I think the part you are missing is not docker-compose configuration, but rather nginx configuration.

The nginx container is listening to the host’s port 80 and forwarding all packages to the nginx server, who’s also listening to port 80 (see listen 80; in nginx’s config)

The nginx server configures a proxy_pass to http://web:8000. This is probably the step that is confusing you. If you want to know more about proxy_pass, nginx has an excellent documentation (https://www.nginx.com/resources/admin-guide/reverse-proxy/)

If you don’t see a benefit for the nginx container you could always remove it and attack the web container directly.

Kind regards,

Reply
Jesus Valdez | enero 27, 2018 10:31 pm

I ask because I have this docker-compose.yml and if I set another port different than 80 it doesn´t work:

version: «3»
services:
nginx:
image: nginx
volumes:
– ./nginx.conf:/etc/nginx/nginx.conf:ro
ports:
– «80:8080»
depends_on:
– web
db:
image: postgres
web:
image: almaral17/linus:latest
deploy:
replicas: 5
resources:
limits:
cpus: «0.1»
memory: 50M
restart_policy:
condition: on-failure
expose:
– «80»
depends_on:
– db
volumes:
– .:/app
env_file:
– .env

Reply

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

*
*