lunes, 15 de julio de 2024

33.- Pasando un proyecto de Django a Producción usando Docker.

Es hora de que pasemos nuestro proyecto a un entorno de producción. Empezaremos configurando Django para que funcione en múltiples entornos y finalmente estableceremos un entorno de producción.

En proyectos del mundo real, tendrás que lidiar con múltiples entornos. Normalmente, tendrás al menos un entorno local para desarrollo y un entorno de producción para servir tu aplicación. También podrías tener otros entornos, como entornos de pruebas o de preparación.

Algunas configuraciones del proyecto serán comunes para todos los entornos, pero otras serán específicas para cada uno. Usualmente, utilizarás un archivo base que define las configuraciones comunes y un archivo de configuración por entorno que sobrescribe las configuraciones necesarias y define otras adicionales.

Gestionaremos los siguientes entornos:

  • local: El entorno local para ejecutar el proyecto en tu máquina.
  • prod: El entorno para desplegar tu proyecto en un servidor de producción.
Para este post puedes usar cualquier proyecto de Django que tengas. Yo voy a usar este en concreto puedes clonarlo si lo necesitas https://github.com/chema-hg/educa.git

Teniendo git instalado yo he creado un directorio llamado "Final". He entrado en ese directorio y ejecutado el siguiente comando para clonarlo:

$ git clone https://github.com/chema-hg/educa.git
            

Empecemos.

Crea un directorio settings/ junto al archivo settings.py del proyecto. Renombra el archivo settings.py a base.py y muévelo al nuevo directorio settings/.

Crea los siguientes archivos adicionales dentro de la carpeta settings/ para que el nuevo directorio se vea así:

settings/
    __init__.py
    base.py
    local.py
    prod.py

Estos archivos son los siguientes:

  • base.py: El archivo de configuración base que contiene configuraciones comunes (anteriormente settings.py).
  • local.py: Configuraciones personalizadas para tu entorno local.
  • prod.py: Configuraciones personalizadas para el entorno de producción.

Has movido los archivos de configuración a un directorio un nivel más abajo, por lo que necesitas actualizar la configuración BASE_DIR en el archivo settings/base.py para que apunte al directorio principal del proyecto.

Al manejar múltiples entornos, crea un archivo de configuración base y un archivo de configuración para cada entorno. Los archivos de configuración de entornos deben heredar las configuraciones comunes y sobrescribir las configuraciones específicas del entorno.

Edita el archivo settings/base.py y reemplaza la siguiente línea:

BASE_DIR = Path(__file__).resolve().parent.parent

con la siguiente:

BASE_DIR = Path(__file__).resolve().parent.parent.parent

Apuntas a un directorio más arriba añadiendo .parent al camino de BASE_DIR. Configuremos las configuraciones para el entorno local.

Configuración del entorno local.


En vez de utilizar las configuraciones por defecto para DEBUG y DATABASES, las definiremos explícitamente para cada entorno. Edita el archivo settings/local.py y añade las siguientes líneas:

proyecto/settings/local.py

from .base import *

DEBUG = True

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

Este es el archivo para la configuración local. En este archivo empezamos importando todas las configuraciones definidas en el archivo base.py y luego especificamos DEBUG y DATABASES especificas de este entorno. Son las mismas que hemos estado usando para un entorno de desarrollo.

Ahora elimina las configuraciones DATABASES y DEBUG del archivo settings/base.py.

El comando de administración de Django no detectará automáticamente el archivo de configuración a usar porque el archivo de configuración del proyecto no es el archivo settings.py predeterminado. Para decirle a Django que use un archivo de configuración especifico tenemos que usar la opción --settings de la siguiente manera:

python manage.py runserver --settings=proyecto.settings.local

nota: "proyecto" es el nombre que tu tengas de tu proyecto, en mi caso es "educa"

NOTA: Si lo ejecutas ahora te dará un error porque mi programa de ejemplo depende del programa REDIS que lo ejecutaremos posteriormente a través de un contenedor de docker..

Si no quieres pasar la opción --settings cada vez que ejecutas el comando de administración, puedes definir una variable de entorno DJANGO_SETTINGS_MODULE. Django la usará para identificar la configuración a usar. Si estas ejecutando Linux o Mac puedes crear esta variable usando el siguiente comando:

export DJANGO_SETTINGS_MODULE=proyecto.settings.local

Acuérdate de sustituir "proyecto" por el nombre real que tenga tu proyecto, en mi caso "educa"

Para que esta variable este disponible para todas las sesiones del shell tendrás que añadir tu "export" al archivo de configuración de tu shell. Este archivo puede variar dependiendo del shell que uses.

Si estás usando windows puedes ejecutar el siguiente comando desde el shell:

set DJANGO_SETTINGS_MODULE=proyecto.settings.local


Configuración para el entorno de producción.

Vamos a empezar añadiendo la configuración inicial del entorno de producción. Edita el archivo proyecto/settings/prod.py y añade el siguiente código:


proyecto/settings/prod.py

from .base import *

DEBUG = False

ADMINS = [
('Perico P', 'email@midominio.com'),
]

ALLOWED_HOSTS = ['*']

DATABASES = {
'default': {
}
}

Estos son los ajustes para el entorno de producción: - DEBUG: Establecer DEBUG en False es necesario para cualquier entorno de producción. No hacerlo resultará en la exposición de información de rastreo y datos de configuración sensibles a todos. - ADMINS: Cuando DEBUG está en False y una vista genera una excepción, toda la información se enviará por correo electrónico a las personas enumeradas en la configuración de ADMINS. Asegúrate de reemplazar la tupla de nombre/correo electrónico con tu propia información. - ALLOWED_HOSTS: Por razones de seguridad, Django solo permitirá que los hosts incluidos en esta lista sirvan el proyecto. Por ahora, permites todos los hosts usando el símbolo de asterisco, *. Limitarás los hosts que pueden usarse para servir el proyecto más adelante. - DATABASES: Aunque por el momento vamos a dejar la configuración de la base de datos por defecto vacía, en las próximas secciones de este post, completarás el archivo de configuración para el entorno de producción usando una base de datos más apropiada para este fin.

Ahora construirás un entorno de producción completo configurando diferentes servicios con Docker.


Usando Docker Compose

Docker te permite construir, desplegar y ejecutar contenedores de aplicaciones. Un contenedor de Docker combina el código fuente de la aplicación con las bibliotecas y dependencias del sistema operativo necesarias para ejecutar la aplicación. Al usar contenedores de aplicaciones, puedes mejorar la portabilidad de tu aplicación. Ya en anteriores post hemos utilizado una imagen de Docker de Redis para servir Redis en tu entorno local. Esta imagen de Docker contiene todo lo necesario para ejecutar Redis y te permite ejecutarlo sin problemas en tu máquina. Para el entorno de producción, usarás Docker Compose para construir y ejecutar diferentes contenedores de Docker. Docker Compose es una herramienta para definir y ejecutar aplicaciones multicontenedor. Puedes crear un archivo de configuración para definir los diferentes servicios y usar un solo comando para iniciar todos los servicios desde tu configuración. Puedes encontrar información sobre Docker Compose en https://docs.docker.com/compose/. Para el entorno de producción, crearás una aplicación distribuida que se ejecuta en varios contenedores de Docker. Cada contenedor de Docker ejecutará un servicio diferente. Inicialmente, definirás los siguientes tres servicios y agregarás servicios adicionales en las siguientes secciones:

- Servicio web: Un servidor web para servir el proyecto Django. - Servicio de base de datos: Un servicio de base de datos para ejecutar PostgreSQL. - Servicio de caché: Un servicio para ejecutar Redis. Comencemos por instalar Docker Compose.

Instalación de Docker Compose

Puedes ejecutar Docker Compose en macOS, Linux de 64 bits y Windows. La forma más rápida de instalar Docker Compose es instalando Docker Desktop. La instalación incluye Docker Engine, la interfaz de línea de comandos y el complemento Docker Compose.

Sin embargo también si tienes instalado Docker Engine y Docker client puedes instalar Docker compose desde la línea de comandos.

Puedes encontrar todas las opciones en https://docs.docker.com/compose/install/

Si optas por instalar Docker Desktop puedes encontrar las instrucciones en https://docs.docker.com/compose/install/compose-desktop/. En cualquier caso necesitaremos crear una imagen de Docker para nuestro proyecto.


Creando un Dockerfile

Necesitas crear una imagen de Docker para ejecutar el proyecto de Django. Un Dockerfile es un archivo de texto que contiene los comandos para que Docker ensamble una imagen de Docker. Prepararemos un Dockerfile con los comandos para construir la imagen de Docker para el proyecto Django. Al lado del directorio del proyecto, educa en mi caso, crea un nuevo archivo y nómbralo Dockerfile. Para que no haya confusión tienes que ponerlo justo encima del directorio que contiene el archivo manage.py. Añade el siguiente código al nuevo archivo:


Dockerfile

FROM python:3.10.12

# Establece las variables de entorno
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Establece el directorio de trabajo
WORKDIR /code

# Añade un usuario específico
RUN adduser --disabled-password --gecos '' miusuario

# Instala las dependencias
RUN pip install --upgrade pip
COPY requirements.txt /code/
RUN pip install -r requirements.txt

# Copia el proyecto de Django y ajusta permisos
COPY . /code/
RUN chown -R miusuario:miusuario /code

# Cambia al usuario no root
USER miusuario

* Sustituye miusuario por el nombre de tu usuario. (en linux puedes verlo con whoiam)

Este código realiza las siguientes tareas:

1. Se utiliza la imagen base de Docker de Python 3.10.12. Puedes encontrar la imagen oficial de Docker de Python en https://hub.docker.com/_/python.

2. Se establecen las siguientes variables de entorno: a. PYTHONDONTWRITEBYTECODE: Evita que Python escriba archivos pyc. b. PYTHONUNBUFFERED: Asegura que los flujos stdout y stderr de Python se envíen directamente al terminal sin ser bufferizados primero.

3. El comando WORKDIR se utiliza para definir el directorio de trabajo de la imagen.

4.- Añadimos un usuario especifico para no tener luego problemas con los permisos de escritura y modificación de los archivos en los contenedores.

5. Se actualiza el paquete pip de la imagen.

6. El archivo requirements.txt se copia al directorio de código de la imagen base de Python.

7. Los paquetes de Python en requirements.txt se instalan en la imagen utilizando pip.

8. El código fuente del proyecto Django se copia desde el directorio local al directorio de código de la imagen y se ajustan los permisos de los archivos.

9.- Se cambia al usuario no root. Con este Dockerfile, has definido cómo se ensamblará la imagen de Docker para servir Django. Puedes encontrar la referencia de Dockerfile en https://docs.docker.com/engine/reference/builder/.


Añadiendo los requisitos de Python

Un archivo requirements.txt se utiliza en el Dockerfile que creaste para instalar todos los paquetes de Python necesarios para el proyecto. Junto al directorio del proyecto, educa en mi caso y al lado del Dockerfile, crea un nuevo archivo y nómbralo requirements.txt. Si has clonado el proyecto puedes encontrar este archivo dentro de "educa/requirements.txt". Copia su contenido y pégalo en este nuevo archivo requirements.txt que acabamos de crear.

En tu proyecto en desarrollo y cuando veas que todo funciona puedes crearlo si quieres con:

pip freeze>requirements.txt

Para que te hagas una idea el mío tiene el siguiente contenido. Tendrás que tener en este archivo todos los paquetes que necesite tu proyecto.

Además de los paquetes de Python que has instalado en los post anteriores, el archivo requirements.txt incluye los siguientes paquetes:

- psycopg2: Un adaptador de PostgreSQL. Usarás PostgreSQL para el entorno de producción. - uwsgi: Un servidor web WSGI. Configurarás este servidor web más adelante para servir Django en el entorno de producción. - daphne: Un servidor web ASGI. Usarás este servidor web más adelante para servir Django Channels.

Las versiones a instalar dependerán de las que hayas usado en tu proyecto.


requirements.txt

asgiref==3.7.2
async-timeout==4.0.3
attrs==23.2.0
autobahn==23.6.2
Automat==22.10.0
certifi==2024.2.2
cffi==1.16.0
channels==4.1.0
charset-normalizer==3.3.2
constantly==23.10.4
cryptography==42.0.8
Django==5.0.2
django-braces==1.15.0
django-debug-toolbar==4.3.0
django-embed-video==1.4.9
django-redisboard==8.4.0
djangorestframework==3.14.0
hyperlink==21.0.0
idna==3.6
incremental==22.10.0
pillow==10.2.0
pyasn1==0.6.0
pyasn1_modules==0.4.0
pycparser==2.22
pymemcache==4.0.0
pyOpenSSL==24.1.0
pytz==2024.1
redis==5.0.3
requests==2.31.0
service-identity==24.1.0
six==1.16.0
sqlparse==0.4.4
Twisted==24.3.0
txaio==23.1.1
typing_extensions==4.9.0
urllib3==2.2.1
zope.interface==6.4.post2
daphne==4.1.2
psycopg2>=2.9.3
uwsgi>=2.0.20

Comencemos configurando la aplicación Docker en Docker Compose. Crearemos un archivo Docker Compose con la definición para el servidor web, la base de datos y los servicios de Redis.


Creando un archivo de Docker Compose.

Para definir los servicios que se ejecutarán en diferentes contenedores de Docker, usaremos un archivo de Docker Compose. Este es un archivo de texto en formato YAML, donde definiremos los servicios, las redes y volúmenes de datos para la aplicación de Docker. Puedes ver un ejemplo de como se construyen archivos YAML a https://yaml.org/.

Al lado del directorio del proyecto, en mi caso educa, crea un nuevo archivo y llámalo docker-compose.yml. Añade el siguiente código:

docker-compose.yml

services:
  web:
    build: .
    command: python /code/educa/manage.py runserver 0.0.0.0:8000
    restart: always
    volumes:
      - .:/code
    ports:
      - "8000:8000"
    environment:
      - DJANGO_SETTINGS_MODULE=educa.settings.prod
     user: "miusuario"  # Añadir el usuario aquí

Es muy importante que copies el código tal cual, incluyendo las tabulaciones. En YAML, las tabulaciones son cruciales y deben de ser consistentes.

En este archivo hemos definido un servicio web. Vamos a explicar uno por uno su contenido

Claro, aquí tienes la traducción al castellano: - build: Define los requisitos de construcción para una imagen de contenedor de servicio. Esto puede ser una cadena única que define una ruta de contexto, o una definición de construcción detallada. Proporcionas una ruta relativa con un solo punto . para apuntar al mismo directorio donde se encuentra el archivo Compose. Docker Compose buscará un Dockerfile en esta ubicación. Puedes leer más sobre la sección build en este enlace (https://docs.docker.com/compose/compose-file/build/).

- command: Sobrescribe el comando predeterminado del contenedor. Ejecutas el servidor de desarrollo de Django usando el comando de gestión runserver. El proyecto se sirve en el host 0.0.0.0, que es la IP predeterminada de Docker, en el puerto 8000.

- restart: Define la política de reinicio para el contenedor. Usando always, el contenedor se reinicia siempre si se detiene. Esto es útil para un entorno de producción, donde deseas minimizar el tiempo de inactividad. Puedes leer más sobre la política de reinicio en este enlace (https://docs.docker.com/config/containers/start-containers-automatically/).

- volumes: Los datos en los contenedores Docker no son permanentes. Cada contenedor Docker tiene un sistema de archivos virtual que se llena con los archivos de la imagen y se destruye cuando el contenedor se detiene. Los volúmenes son el método preferido para persistir datos generados y utilizados por los contenedores Docker. En esta sección, montas el directorio local . en el directorio /code de la imagen. Puedes leer más sobre los volúmenes de Docker en este enlace (https://docs.docker.com/storage/volumes/).

- ports: Expone puertos del contenedor. El puerto 8000 del host se asigna al puerto 8000 del contenedor, en el que se está ejecutando el servidor de desarrollo de Django.

- environment: Define variables de entorno. Configuras la variable de entorno DJANGO_SETTINGS_MODULE para usar el archivo de configuración de producción de Django educa.settings.prod.

- user: al especificar el usuario en el archivo "docker-compose.yml" para el servicio web, garantizamos que los procesos dentro de ese contenedor se ejcuten con el usuario creado en el Dockerfile ("miusuario"). Ten en cuenta que en la definición del archivo Docker Compose, estás utilizando el servidor de desarrollo de Django para servir la aplicación. El servidor de desarrollo de Django no es adecuado para uso en producción, por lo que lo reemplazarás más adelante con un servidor web WSGI de Python. Puedes encontrar información sobre la especificación de Docker Compose en este enlace (https://docs.docker.com/compose/compose-file/). En este punto, asumiendo que tu directorio padre se llama "educa" es decir el nombre del proyecto, la estructura básica de archivos debería verse de la siguiente manera:

educa/
    docker-compose.yml
    Dockerfile
    educa/
        manage.py
        ....
    requirements.txt

Abre una sesión de shell en este directorio padre, donde se encuentra el archivo docker-compose.yml, y ejecuta el siguiente comando:

$ docker compose up

* Ejecuta como root si no has añadido tu usuario al grupo Docker.

Si no quieres usar el comando sudo cada vez, puedes añadir tu usuario al grupo docker. Esto le dará a tu usuario permisos para interactuar con el demonio de Docker.

Sigue estos pasos:

  1. Añade tu usuario al grupo docker:

    sudo usermod -aG docker $USER
  2. Cierra la sesión y vuelve a iniciarla, o ejecuta el siguiente comando para aplicar los cambios sin cerrar sesión:

    newgrp docker
  3. Verifica que tu usuario está en el grupo docker:

    id -nG

Después de hacer esto, intenta ejecutar nuevamente el comando docker compose up.

Otra cosa a verificar son los permisos del socket /var/run/docker.sock. Asegúrate de que los permisos sean correctos:

ls -l /var/run/docker.sock

El resultado debe mostrar algo como esto:

srw-rw---- 1 root docker 0 Jun 21 12:34 /var/run/docker.sock

Asegúrate de que el socket es accesible por el grupo docker y de que tu usuario pertenece a dicho grupo.

Al ejecutarse este comando se definirá el contenedor de docker definido en el archivo docker compose.

Volviendo a lo que estábamos haciendo, cuando finalice de ejecutarse "docker compose up" tendrás el contenedor de tu proyecto ejecutándose.

Los estilos CSS no se están cargando. Estás usando `DEBUG=False`, por lo que los patrones de URL para servir archivos estáticos no están siendo incluidos en el archivo `urls.py` principal del proyecto. Recuerda que el servidor de desarrollo de Django no es adecuado para servir archivos estáticos. Configurarás un servidor para servir archivos estáticos más adelante en este post.

Si accedes a cualquier URL de tu sitio, podrías obtener un error HTTP 500 porque aún no has configurado una base de datos para el entorno de producción. Echa un vistazo a la ejecución del siguiente comando:

$sudo docker ps

SALIDA:

CONTAINER ID   IMAGE       COMMAND                  CREATED        STATUS         PORTS                                       NAMES

8f3422ed977b   final-web   "python /code/educa/…"   23 hours ago   Up 5 minutes   0.0.0.0:8000->8000/tcp, :::8000->8000/tcp   final-web-1


Como vemos tenemos un contenedor en ejecución llamado final-web el cual está corriendo en el puerto 8000. El nombre de la aplicación de Docker se genera dinámicamente usando el nombre del directorio en el que se encuentra el archivo docker-compile, en mi caso "final".

Si quieres ver la imagen generada de la cual se crea el contenedor puedes usar el comando:

$ sudo docker images -a

SALIDA:

REPOSITORY TAG IMAGE ID CREATED SIZE final-web latest ef2cccc8385d 23 hours ago 1.26GB


Si quieres para la ejecución de los contenedores puedes usar CRTL + C. Si lo que quieres es parar los contenedores y eliminarlos (lo cual no eliminará las imágenes) puedese usar:

$ docker compose down

A continuación vamos a añadir el servicio PostgreSQL y el servicio REDIS a la aplicación.

Configuración del servicio PostgreSQL

A lo largo de este libro, has utilizado principalmente la base de datos SQLite. SQLite es simple y rápida de configurar, pero para un entorno de producción, necesitarás una base de datos más potente, como PostgreSQL, MySQL u Oracle. Vimos cómo instalar PostgreSQL en el POST "7.- BASES DE DATOS - POSTGRESQL". Para el entorno de producción, utilizaremos una imagen Docker de PostgreSQL en su lugar. Puedes encontrar información sobre la imagen oficial de Docker de PostgreSQL en este enlace (https://hub.docker.com/_/postgres). Edita el archivo docker-compose.yml y añade las siguientes líneas resaltadas en negrita:

docker-compose.yml

services:
  db:
    image: postgres:14.5
    restart: always
    volumes:
      - ./data/db:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres

  web:
    build: .
    command: python /code/educa/manage.py runserver 0.0.0.0:8000
    restart: always
    volumes:
      - .:/code
    ports:
      - "8000:8000"
    environment:
      - DJANGO_SETTINGS_MODULE=educa.settings.prod
      - POSTGRES_DB=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    depends_on:
      - db
    user: "miusuario"

Con estos cambios, defines un servicio llamado db con las siguientes subsecciones:

- image: El servicio utiliza la imagen base de Docker de postgres.

- restart: La política de reinicio se establece en always.

- volumes: Montas el directorio ./data/db en el directorio /var/lib/postgresql/data de la imagen para persistir la base de datos, de modo que los datos almacenados en la base de datos se mantengan después de que la aplicación Docker se detenga. Esto creará la ruta local data/db/.

- environment: Utilizas las variables POSTGRES_DB (nombre de la base de datos), POSTGRES_USER y POSTGRES_PASSWORD con valores predeterminados.

La definición del servicio web ahora incluye las variables de entorno de PostgreSQL para Django. Creas una dependencia de servicio usando depends_on para que el servicio web se inicie después del servicio db. Esto garantizará el orden de la inicialización del contenedor, pero no garantizará que PostgreSQL esté completamente iniciado antes de que el servidor web de Django se inicie. Para resolver esto, necesitas usar un script que esperará la disponibilidad del host de la base de datos y su puerto TCP. Docker recomienda usar la herramienta wait-for-it para controlar la inicialización del contenedor. Descarga el script Bash wait-for-it.sh desde este enlace (https://github.com/vishnubob/wait-for-it/blob/master/wait-for-it.sh) y guarda el archivo junto al archivo docker-compose.yml.

Asegúrate de dar permisos de ejecución al archivo con: $ chmod +x wait_for_it.sh

Luego edita el archivo docker-compose.yml y modifica la definición del servicio web como se indica a continuación. El nuevo código está resaltado en negrita:

docker-compose.yml

web:
    build: .
    command: ["./wait_for_it.sh", "db:5432", "--",
              "python", "/code/educa/manage.py", "runserver",
              "0.0.0.0:8000"]
    restart: always
    volumes:
      - .:/code
    ports:
      - "8000:8000"
    environment:
      - DJANGO_SETTINGS_MODULE=educa.settings.prod
      - POSTGRES_DB=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    depends_on:
      - db
    user: "miusuario"

En esta definición de servicio, utilizas el script Bash wait-for-it.sh para esperar a que el host db esté listo y acepte conexiones en el puerto 5432, el puerto predeterminado para PostgreSQL, antes de iniciar el servidor de desarrollo de Django. Puedes leer más sobre el orden de inicio de servicios en Compose en este enlace (https://docs.docker.com/compose/startup-order/).

Vamos a editar la configuración de Django. Edita el archivo educa/settings/prod.py y añade el siguiente código resaltado en negrita:

educa/settings/prod.py

import os

from .base import *

DEBUG = False

ADMINS = [
('Perico P', 'email@midominio.com'),
]

ALLOWED_HOSTS = ['*']

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('POSTGRES_DB'),
'USER': os.environ.get('POSTGRES_USER'),
'PASSWORD': os.environ.get('POSTGRES_PASSWORD'),
'HOST': 'db',
'PORT': 5432,
}
}
En el archivo de configuración de producción, utilizas las siguientes configuraciones:

• ENGINE: Usas el backend de base de datos de Django para PostgreSQL.

• NAME, USER, y PASSWORD: Usas os.environ.get() para recuperar las variables de entorno POSTGRES_DB (nombre de la base de datos), POSTGRES_USER y POSTGRES_PASSWORD. Has establecido estas variables de entorno en el archivo Docker Compose.

• HOST: Usas db, que es el nombre de host del contenedor para el servicio de base de datos definido en el archivo Docker Compose. El nombre de host de un contenedor por defecto es el ID del contenedor en Docker. Por eso usas el nombre de host db.

• PORT: Usas el valor 5432, que es el puerto predeterminado para PostgreSQL.

Detén la aplicación de Docker desde la terminal presionando las teclas Ctrl + C o usando el botón de detener en la aplicación Docker Desktop. Luego, inicia Compose nuevamente con el comando:

docker compose up

La primera ejecución después de agregar el servicio db al archivo Docker Compose tomará más tiempo porque PostgreSQL necesita inicializar la base de datos.

Tanto la base de datos PostgreSQL como la aplicación Django están listas. La base de datos de producción está vacía, por lo que necesitaremos aplicar las migraciones de la base de datos.


Aplicando migraciones de base de datos y creando un superusuario

Abre una terminal diferente en el directorio padre, donde se encuentra el archivo docker-compose.yml, y ejecuta el siguiente comando:

docker compose exec web python /code/educa/manage.py migrate

El comando docker compose exec te permite ejecutar comandos en el contenedor. Usas este comando para ejecutar el comando de administración migrate en el contenedor web de Docker.

Finalmente, crea un superusuario con el siguiente comando:

docker compose exec web python /code/educa/manage.py createsuperuser

Se han aplicado las migraciones a la base de datos y has creado un superusuario. Puedes acceder a http://localhost:8000/admin/ con las credenciales del superusuario, aunque seguramente te de un error si estas usando mi imagen de ejemplo porque aún no hemos configurado el contenedor para REDIS. Los estilos CSS aún no se cargarán porque no has configurado el servicio de archivos estáticos todavía.

Has definido servicios para servir Django y PostgreSQL usando Docker Compose. A continuación, agregarás un servicio para servir Redis en el entorno de producción.


Configurando el servicio de REDIS.


Vamos a añadir REDIS al archivo de Docker Compose. Para ello utilizaremos la imagen oficial de REDIS que nos proporciona Docker. Puedes encontrar más información sobre la misma en:

https://hub.docker.com/_/redis

Edita el archivo docker-compose.yml y añade el siguiente código:

docker-compose.yml

services:
  db:
    #...
  cache:
    image: redis:7.0.4
    restart: always
    volumes:
      - ./data/cache:/data 

  web:
    #....
    depends_on:
      - db
      - cache



En el código anterior, defines el servicio de caché con las siguientes subsecciones:

- image: El servicio utiliza la imagen base de Docker de Redis.
- restart: La política de reinicio está configurada para siempre.
- volumes: Montas el directorio `./data/cache` en el directorio de la imagen `/data`, donde se guardarán las escrituras de Redis. Esto creará la ruta local `data/cache/`.


En la definición del servicio web, añades el servicio de caché como una dependencia, para que el servicio web se inicie después del servicio de caché. El servidor Redis se inicializa rápidamente, por lo que en este caso no necesitas usar la herramienta wait-for-it.

Edita el archivo `educa/settings/prod.py` y añade las siguientes líneas:

REDIS_URL = 'redis://cache:6379'
CACHES['default']['LOCATION'] = REDIS_URL
CHANNEL_LAYERS['default']['CONFIG']['hosts'] = [REDIS_URL]


En estas configuraciones, utilizas el nombre de host de la caché que se genera automáticamente por Docker Compose usando el nombre del servicio de caché y el puerto 6379 utilizado por Redis. Modificas la configuración de CACHE de Django y la configuración de CHANNEL_LAYERS utilizada por Channels para usar la URL de Redis en producción.

Detén la aplicación Docker desde la terminal presionando las teclas Ctrl + C o usando el botón de detener en la aplicación Docker Desktop. Luego, inicia Compose nuevamente con el comando:

docker compose up

Si observas cuales son los contenedores en ejecución verás los siguientes:

CONTAINER ID   IMAGE           COMMAND                  CREATED        STATUS          PORTS                                       NAMES
fa58bfd42098   final-web       "./wait-for-it.sh db…"   19 hours ago   Up 15 minutes   0.0.0.0:8000->8000/tcp, :::8000->8000/tcp   final-web-1
2611603348f8   redis:7.0.4     "docker-entrypoint.s…"   19 hours ago   Up 15 minutes   6379/tcp                                    final-cache-1
7d509cdd19a1   postgres:14.5   "docker-entrypoint.s…"   45 hours ago   Up 15 minutes   5432/tcp                                    final-db-1

La aplicación de docker ejecuta un contenedor para cada uno de los servicios definidos en el archivo docker compose: db, cache y web.

Aun estamos sirviendo Django con el servidor de desarrollo de Django, el cual no es adecuado para su uso como en producción. Vamos a reemplazarlo con el servidor WSGI de python.

Servir Django a través de WSGI y NGINX


La plataforma principal de despliegue de Django es WSGI. WSGI significa Web Server Gateway Interface y es el estándar para servir aplicaciones Python en la web.

Cuando generas un nuevo proyecto usando el comando `startproject`, Django crea un archivo `wsgi.py` dentro de tu directorio de proyecto. Este archivo contiene un callable de la aplicación WSGI, que es un punto de acceso a tu aplicación.

WSGI se usa tanto para ejecutar tu proyecto con el servidor de desarrollo de Django como para desplegar tu aplicación con el servidor de tu elección en un entorno de producción. Puedes aprender más sobre WSGI en https://wsgi.readthedocs.io/en/latest/.

Usando uWSGI


uWSGI es un servidor de aplicaciones Python extremadamente rápido. Se comunica con tu aplicación Python usando la especificación WSGI. uWSGI traduce las solicitudes web en un formato que tu proyecto Django puede procesar.

Vamos a configurar uWSGI para servir el proyecto Django. Ya agregaste `uwsgi==2.0.20` al archivo `requirements.txt` del proyecto, por lo que uWSGI ya se está instalando en la imagen Docker del servicio web.

Edita el archivo `docker-compose.yml` y modifica la definición del servicio web como sigue. El nuevo código está resaltado en azul:

docker-compose.yml

  web:
    build: .
    command: ["./wait-for-it.sh", "db:5432", "--",
              "uwsgi", "--ini", "/code/config/uwsgi/uwsgi.ini"]

    restart: always
    volumes:
      - .:/code
    environment:
      - DJANGO_SETTINGS_MODULE=educa.settings.prod
      - POSTGRES_DB=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    depends_on:
      - db
      - cache
    user: "miusuario"

Asegúrate de eliminar la sección "ports". uWSGI será accesible mediante un socket, por lo que no necesitas exponer un puerto en el contenedor.

El nuevo comando para la imagen ejecuta uWSGI pasando el archivo de configuración `/code/config/uwsgi/uwsgi.ini` a él. Vamos a crear el archivo de configuración para uWSGI.

Configurando uWSGI


uWSGI te permite definir una configuración personalizada en un archivo .ini. Junto al archivo `docker-compose.yml`, crea la ruta del archivo `config/uwsgi/uwsgi.ini`. Asumiendo que mi directorio padre se llama `Final`, la estructura del archivo debería verse de la siguiente manera:

Final/
    config/
        uwsgi/
            uwsgi.ini
    docker-compose.yml
    Dockerfile
    educa/
        manage.py
        ...
    requirements.txt

Edita el archivo uwsgi.ini que acabamos de crear y añade el siguiente código:

uwsgi.ini

[uwsgi]
socket=/code/educa/uwsgi_app.sock chdir = /code/educa/ module=educa.wsgi:application master=true chmod-socket=666 uid=miusuario gid=miusuario vacuum=true

En el archivo uwsgi.ini, defines las siguientes opciones:

  • socket: El socket UNIX/TCP al que se enlaza el servidor.
  • chdir: La ruta a tu directorio de proyecto, para que uWSGI cambie a ese directorio antes de cargar la aplicación Python.
  • module: El módulo WSGI a utilizar. Configuras esto con el callable de la aplicación contenido en el módulo WSGI de tu proyecto.
  • master: Habilita el proceso maestro.
  • chmod-socket: Los permisos de archivo para aplicar al archivo de socket. En este caso, usas 666 para que NGINX pueda leer/escribir en el socket.
  • uid: El ID de usuario del proceso una vez iniciado.
  • gid: El ID de grupo del proceso una vez iniciado.
  • vacuum: Usar true indica a uWSGI que limpie cualquier archivo temporal o sockets UNIX que cree.

La opción socket está destinada a la comunicación con algún enrutador de terceros, como NGINX. Vas a ejecutar uWSGI usando un socket y vas a configurar NGINX como tu servidor web, que se comunicará con uWSGI a través del socket. Puedes encontrar la lista de opciones disponibles de uWSGI en https://uwsgi-docs.readthedocs.io/en/latest/Options.html. No podrás acceder a tu instancia de uWSGI desde tu navegador ahora, ya que se está ejecutando a través de un socket. Completemos el entorno de producción.

Usando NGINX

Cuando sirves un sitio web, tienes que servir contenido dinámico, pero también necesitas servir archivos estáticos, como hojas de estilo CSS, archivos JavaScript e imágenes. Aunque uWSGI es capaz de servir archivos estáticos, agrega una carga innecesaria a las solicitudes HTTP y, por lo tanto, se recomienda configurar un servidor web, como NGINX, frente a él. NGINX es un servidor web enfocado en alta concurrencia, rendimiento y bajo uso de memoria. NGINX también actúa como un proxy inverso, recibiendo solicitudes HTTP y WebSocket y enrutándolas a diferentes backends. Generalmente, usarás un servidor web, como NGINX, frente a uWSGI para servir archivos estáticos de manera eficiente, y reenviarás las solicitudes dinámicas a los trabajadores de uWSGI. Al usar NGINX, también puedes aplicar diferentes reglas y beneficiarte de sus capacidades de proxy inverso.

Agregaremos el servicio NGINX al archivo Docker Compose usando la imagen oficial de Docker de NGINX. Puedes encontrar información sobre la imagen oficial de Docker de NGINX en https://hub.docker.com/_/nginx. Edita el archivo docker-compose.yml y agrega las siguientes líneas resaltadas en negrita:

docker-compose.yml

services:
  db:
    #...
  cache:
    #...  

  web:
    #...
nginx: image: nginx:1.23.1 restart: always volumes: - ./config/nginx:/etc/nginx/templates - .:/code ports: - "80:80"

Has añadido la definición para el servicio nginx con las siguientes subsecciones:

  • image: El servicio utiliza la imagen base de Docker de nginx.
  • restart: La política de reinicio está configurada para siempre.
  • volumes: Montas el volumen ./config/nginx en el directorio /etc/nginx/templates de la imagen de Docker. Aquí es donde NGINX buscará una plantilla de configuración predeterminada. También montas el directorio local . en el directorio /code de la imagen, para que NGINX pueda tener acceso a los archivos estáticos.
  • ports: Expones el puerto 80, que se asigna al puerto 80 del contenedor. Este es el puerto predeterminado para HTTP.

Vamos a configurar el servidor web NGINX.


Configurando Nginx.


Dentro del directorio /config/ crea el siguiente directorio resaltado en azul. Debería tener este aspecto:

config/
    uwsgi/
        uwsgi.ini
    nginx/
        default.conf.template

Edita el archivo default.conf.template y añade el siguiente código:

default.conf.template

# upstream for uWSGI
upstream uwsgi_app {
server unix:/code/educa/uwsgi_app.sock;
}
server {
listen 80;
server_name www.educaproject.com educaproject.com;
error_log stderr warn;
access_log /dev/stdout main;
location / {
include /etc/nginx/uwsgi_params;
uwsgi_pass uwsgi_app;
}
}
Esta es la configuración básica para NGINX. En esta configuración, configuras un upstream llamado uwsgi_app, que apunta al socket creado por uWSGI. Utilizas el bloque del servidor con la siguiente configuración:

- Indicas a NGINX que escuche en el puerto 80.

- Configuras el nombre del servidor a ambos www.educaproject.com y educaproject.com. NGINX servirá las solicitudes entrantes para ambos dominios.

- Usas stderr para la directiva error_log para obtener los registros de errores escritos en el archivo de error estándar. El segundo parámetro determina el nivel de registro. Usas warn para obtener advertencias y errores de mayor severidad.

- Apuntas access_log a la salida estándar con /dev/stdout.

- Especificas que cualquier solicitud bajo la ruta / debe ser enrutada al socket uwsgi_app hacia uWSGI.

- Incluyes los parámetros de configuración predeterminados de uWSGI que vienen con NGINX. Estos están ubicados en /etc/nginx/uwsgi_params.

NGINX ahora está configurado. Puedes encontrar la documentación de NGINX en https://nginx.org/en/docs/. Detén la aplicación Docker desde la terminal presionando las teclas Ctrl + C o utilizando el botón de detener en la aplicación Docker Desktop. Luego, inicia Compose nuevamente con el comando:

```
docker compose up
```

Abre la URL http://localhost/ en tu navegador. No es necesario agregar un puerto a la URL porque estás accediendo al host a través del puerto HTTP estándar 80. Deberías ver la página de la lista de cursos sin estilos CSS:

The course list page served with NGINX and uWSGI



El siguiente diagrama muestra el ciclo de peticiones y respuestas en el entorno de producción que hemos configurado:

The production environment request/response cycle The following

Si compruebas los contenedores que estamos usando hasta ahora tenemos cuatro:
1.- El servicio db que corre PosgreSQL.
2.- El servicio cache que corre Redis.
3.- El servicio web que ejecuta uWSGI+Django
4.- El servicio nginx que ejecuta Nginx.

Vamos a continuar. Lo siguiente que haremos será entrar en vez de con localhost, configuraremos el proyecto para que utilice el nombre de host que utilicemos en este caso por ejemplo www.educaproject.com


Usando un hostname.


Usaré el nombre de host educaproject.com para mi sitio. Como estamos utilizando un nombre de dominio de muestra, no es un dominio de host real, necesitaremos redirigirlo a nuestro localhost.

Si estás utilizando Linux o macOS, edita el archivo /etc/hosts y agrega la siguiente línea:

127.0.0.1 educaproject.com www.educaproject.com

Si estás utilizando Windows, edita el archivo C:\Windows\System32\drivers\etc y agrega la misma línea.

Al hacer esto, estás redirigiendo los nombres de host educaproject.com y www.educaproject.com a tu servidor local. En un servidor de producción, no necesitarás hacer esto, ya que tendrás una dirección IP fija y apuntarás tu nombre de host a tu servidor en la configuración DNS de tu dominio.

Abre http://educaproject.com/ en tu navegador. Deberías poder ver tu sitio, aún sin ningún recurso estático cargado. Tu entorno de producción está casi listo.

Ahora puedes restringir los hosts que pueden servir tu proyecto Django. Edita el archivo de configuración de producción educa/settings/prod.py de tu proyecto y cambia la configuración de ALLOWED_HOSTS, de la siguiente manera:

ALLOWED_HOSTS = ['educaproject.com', 'www.educaproject.com']

Django solo servirá tu aplicación si está ejecutándose bajo alguno de estos nombres de host. Puedes leer más sobre la configuración de ALLOWED_HOSTS en https://docs.djangoproject.com/en/4.1/ref/settings/#allowed-hosts.

El entorno de producción está casi listo. Continuemos configurando NGINX para servir archivos estáticos.

Servir archivos estáticos y de medios


uWSGI es capaz de servir archivos estáticos perfectamente, pero no es tan rápido y efectivo como NGINX. Para obtener el mejor rendimiento, usaremos NGINX para servir archivos estáticos en el entorno de producción. Configuraremos NGINX para servir tanto los archivos estáticos de tu aplicación (hojas de estilo CSS, archivos JavaScript e imágenes) como los archivos de medios subidos por los instructores para los contenidos del curso.

Edita el archivo "settings/base.py" y agrega la siguiente línea justo debajo de la configuración de `STATIC_URL`:

"""
STATIC_ROOT = BASE_DIR / 'static'
"""

Este es el directorio raíz para todos los archivos estáticos del proyecto. A continuación, recopilaremos los archivos estáticos de las diferentes aplicaciones de Django en el directorio común.

Recopilación de archivos estáticos


Cada aplicación en tu proyecto Django puede contener archivos estáticos en un directorio "static/". Django proporciona un comando para recopilar archivos estáticos de todas las aplicaciones en una sola ubicación. Esto simplifica la configuración para servir archivos estáticos en producción. El comando `collectstatic` recopila los archivos estáticos de todas las aplicaciones del proyecto en la ruta definida con la configuración `STATIC_ROOT`.

Detén la aplicación Docker desde la terminal presionando las teclas `Ctrl + C` o usando el botón de detener en la aplicación Docker Desktop o tambien con "docker compose down". Luego, inicia Compose nuevamente con el comando:

'''
docker compose up
'''

Abre otra terminal en el directorio padre, donde se encuentra el archivo `docker-compose.yml`, y ejecuta el siguiente comando:

```
docker compose exec web python /code/educa/manage.py collectstatic
```

Ten en cuenta que alternativamente puedes ejecutar el siguiente comando en la terminal, desde el directorio del proyecto `educa`:

```
python manage.py collectstatic --settings=educa.settings.local
```

Ambos comandos tendrán el mismo efecto ya que el directorio base local está montado en la imagen Docker. Django te preguntará si deseas sobrescribir cualquier archivo existente en el directorio raíz. Escribe `yes` y presiona Enter. Verás la siguiente salida:

```
171 static files copied to '/code/educa/static'.
```

Los archivos ubicados en el directorio `static/` de cada aplicación presente en la configuración `INSTALLED_APPS` se han copiado al directorio global del proyecto `/educa/static/`.

Servir archivos estáticos con NGINX


Edita el archivo "config/nginx/default.conf.template" y agrega las siguientes líneas resaltadas en azul al bloque `server`:

default.conf.template

server {
# ...
location / {
include /etc/nginx/uwsgi_params;
uwsgi_pass uwsgi_app;
}
location /static/ {
alias /code/educa/static/;
}
location /media/ {
alias /code/educa/media/;
}
}

Estas directivas le indican a NGINX que sirva directamente los archivos estáticos ubicados en las rutas /static/ y /media/.

Estas rutas son las siguientes:

- /static/: Corresponde a la ruta de la configuración `STATIC_URL`. La ruta de destino corresponde al valor de la configuración `STATIC_ROOT`. Se utiliza para servir los archivos estáticos de tu aplicación desde el directorio montado en la imagen Docker de NGINX.

- /media/: Corresponde a la ruta de la configuración `MEDIA_URL`, y su ruta de destino corresponde al valor de la configuración `MEDIA_ROOT`. Se utiliza para servir los archivos de medios subidos para los contenidos del curso desde el directorio montado en la imagen Docker de NGINX.

El esquema del entorno de producción ahora se ve así:

The production environment request/response cycle, including static files



Los archivos bajo las rutas /static/ y /media/ ahora son servidos directamente por NGINX, en lugar de ser reenviados a uWSGI. Las solicitudes a cualquier otra ruta todavía son pasadas por NGINX a uWSGI a través del socket UNIX.

Detén la aplicación Docker desde la terminal presionando las teclas `Ctrl + C` o usando el botón de detener en la aplicación Docker Desktop. Luego, inicia Compose nuevamente con el comando:

```
docker compose up
```

Abre http://educaproject.com/ en tu navegador. Deberías ver la siguiente pantalla:

The course list page served with NGINX and uWSGI



Los recursos estáticos, como hojas de estilo CSS e imágenes, ahora se cargan correctamente. Las solicitudes HTTP para archivos estáticos ahora son servidas directamente por NGINX, en lugar de ser reenviadas a uWSGI.

Hemos configurado exitosamente NGINX para servir archivos estáticos. A continuación, verificaremos el proyecto Django para desplegarlo en un entorno de producción y vas a servir tu sitio bajo HTTPS.

Asegurar tu sitio con SSL/TLS

El protocolo de Seguridad de la Capa de Transporte (TLS) es el estándar para servir sitios web a través de una conexión segura. El predecesor de TLS es Secure Sockets Layer (SSL). Aunque SSL está ahora obsoleto, encontrarás referencias a ambos términos, TLS y SSL, en varias bibliotecas y documentación en línea.

Se recomienda encarecidamente que sirvas tus sitios web a través de HTTPS.

Comprobando tu proyecto para prepararlo para la producción.


En esta sección, vas a verificar tu proyecto Django para un despliegue en producción y preparar el proyecto para ser servido sobre HTTPS. Luego, vas a configurar un certificado SSL/TLS en NGINX para servir tu sitio de manera segura.

Verificando tu proyecto para producción

Django incluye un marco de verificación del sistema para validar tu proyecto en cualquier momento. Este marco inspecciona las aplicaciones instaladas en tu proyecto Django y detecta problemas comunes. Las verificaciones se activan implícitamente al ejecutar comandos de gestión como runserver y migrate. Sin embargo, también puedes activar las verificaciones explícitamente con el comando de gestión check.

Puedes leer más sobre el marco de verificación del sistema de Django en https://docs.djangoproject.com/es/4.1/topics/checks/.

Confirmemos que el marco de verificación no genera problemas para tu proyecto. Abre la terminal en el directorio del proyecto educa y ejecuta el siguiente comando para verificar tu proyecto:

python manage.py check --settings=educa.settings.prod

Aunque también se puede montar los contenedores y ejecutar esta orden dentro de un shell del contenedor final-web con "docker exec -it identificación_contenedor bash".

Deberías ver el siguiente resultado:

El marco de verificación del sistema no identificó problemas (0 silenciados).

El marco de verificación del sistema no encontró ningún problema. Si usas la opción --deploy, el marco de verificación del sistema realizará verificaciones adicionales relevantes para un despliegue en producción.

Ejecuta el siguiente comando desde el directorio del proyecto educa:

python manage.py check --deploy --settings=educa.settings.prod

Verás un resultado similar al siguiente:

:/code/educa$ python manage.py check --deploy --settings=educa.settings.prod
System check identified some issues:

WARNINGS:
?: (security.W004) You have not set a value for the SECURE_HSTS_SECONDS setting. If your entire site is served only over SSL, you may want to consider setting a value and enabling HTTP Strict Transport Security. Be sure to read the documentation first; enabling HSTS carelessly can cause serious, irreversible problems.
?: (security.W008) Your SECURE_SSL_REDIRECT setting is not set to True. Unless your site should be available over both SSL and non-SSL connections, you may want to either set this setting True or configure a load balancer or reverse-proxy server to redirect all connections to HTTPS.
?: (security.W009) Your SECRET_KEY has less than 50 characters, less than 5 unique characters, or it's prefixed with 'django-insecure-' indicating that it was generated automatically by Django. Please generate a long and random value, otherwise many of Django's security-critical features will be vulnerable to attack.
?: (security.W012) SESSION_COOKIE_SECURE is not set to True. Using a secure-only session cookie makes it more difficult for network traffic sniffers to hijack user sessions.
?: (security.W016) You have 'django.middleware.csrf.CsrfViewMiddleware' in your MIDDLEWARE, but you have not set CSRF_COOKIE_SECURE to True. Using a secure-only CSRF cookie makes it more difficult for network traffic sniffers to steal the CSRF token.

System check identified 5 issues (0 silenced).

El marco de verificación ha identificado cinco problemas (0 errores, 5 advertencias). Todas las advertencias están relacionadas con configuraciones de seguridad.

Vamos a abordar el problema security.W009. Edita el archivo educa/settings/base.py y modifica la configuración de SECRET_KEY eliminando el prefijo django-insecure- y agregando caracteres aleatorios adicionales para generar una cadena de al menos 50 caracteres.

Ejecuta nuevamente el comando check y verifica que el problema security.W009 ya no se presenta. El resto de las advertencias están relacionadas con la configuración de SSL/TLS. Las abordaremos a continuación.

Configuración de tu proyecto Django para SSL/TLS


Django viene con configuraciones específicas para el soporte de SSL/TLS. Vamos a editar la configuración de producción para servir tu sitio a través de HTTPS.

Edita el archivo de configuración `educa/settings/prod.py` y agrega las siguientes configuraciones:

```
# Seguridad
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = True
```

Estas configuraciones son las siguientes:

- CSRF_COOKIE_SECURE: Usa una cookie segura para la protección contra falsificación de solicitudes entre sitios (CSRF). Con `True`, los navegadores solo transferirán la cookie a través de HTTPS.
- SESSION_COOKIE_SECURE: Usa una cookie de sesión segura. Con `True`, los navegadores solo transferirán la cookie a través de HTTPS.
- SECURE_SSL_REDIRECT: Indica si las solicitudes HTTP deben ser redirigidas a HTTPS.

Django ahora redirigirá las solicitudes HTTP a HTTPS; las cookies de sesión y CSRF solo se enviarán a través de HTTPS.

Ejecuta el siguiente comando desde el directorio principal de tu proyecto:

```
python manage.py check --deploy --settings=educa.settings.prod
```

Solo queda una advertencia, `security.W004`:

```
(security.W004) You have not set a value for the SECURE_HSTS_SECONDS setting.
```

Esta advertencia está relacionada con la política de Seguridad de Transporte Estricto de HTTP (HSTS). La política HSTS evita que los usuarios eviten las advertencias y se conecten a un sitio con un certificado SSL caducado, autofirmado o de otra manera no válido. En la siguiente sección, utilizaremos un certificado autofirmado para nuestro sitio, por lo que ignoraremos esta advertencia.

Cuando tengas un dominio real, puedes solicitar una Autoridad de Certificación (CA) confiable para que emita un certificado SSL/TLS para él, de modo que los navegadores puedan verificar su identidad. En ese caso, puedes darle un valor a `SECURE_HSTS_SECONDS` mayor que 0, que es el valor predeterminado. Puedes obtener más información sobre la política HSTS en https://docs.djangoproject.com/en/4.1/ref/middleware/#http-strict-transport-security.

Hemos corregido con éxito el resto de los problemas planteados por el marco de verificación. Puedes leer más sobre la lista de verificación de implementación de Django en https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/.


Creación de un certificado SSL/TLS


Crea un nuevo directorio dentro del directorio del proyecto 'educa' (a la misma altura que manage,py, ) y llámalo 'ssl'. Luego, genera un certificado SSL/TLS desde la línea de comandos con el siguiente comando:

"""
openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
-keyout ssl/educa.key -out ssl/educa.crt \
-subj '/CN=*.educaproject.com' \
-addext 'subjectAltName=DNS:*.educaproject.com'
"""

Esto generará una clave privada y un certificado SSL/TLS de 2048 bits válido por 10 años. Este certificado se emite para el nombre de host `*.educaproject.com`. Este es un certificado comodín; al usar el carácter comodín `*` en el nombre de dominio, el certificado se puede usar para cualquier subdominio de `educaproject.com`, como `www.educaproject.com` o `django.educaproject.com`. Después de generar el certificado, el directorio "educa/ssl/" contendrá dos archivos: `educa.key` (la clave privada) y `educa.crt` (el certificado).

Necesitarás al menos OpenSSL 1.1.1 o LibreSSL 3.1.0 para usar la opción "-addext". Puedes verificar la ubicación de OpenSSL en tu máquina con el comando "which openssl" y puedes verificar la versión con el comando "openssl version".


Configurando Nginx para que use SSL/TLS


Edita el archivo docker-compose.yml y añade la línea resaltada en azul:

docker-compose.yml

services:
    # ...
    
    nginx:
        #...
        ports:
            - "80:80"
            - "443:443"

Con esto el conenedor del host de Nginx será acesible tanto en el puerto 80 (HTTP) como en el puerto 443 (HTTPS). Asociamos el host del puerto 443 con el puerto del contenedor 443.

Ahora edita el archivo de configuración de Nginx /config/nginx/default.conf.template y edita el bloque del servidor para poder utilizar SSL/TLS de la siguiente forma:

default.conf.template

server {
listen 80;
listen 443 ssl;
ssl_certificate /code/educa/ssl/educa.crt;
ssl_certificate_key /code/educa/ssl/educa.key;
server_name www.educaproject.com educaproject.com;
# ...
Con esto conseguimos que Nginx escuche las peticiones en ambos puertos, HTTP en el puerto 80 y HTTPS en el puerto 443. También le hemos indicado el camino donde encontrar los certificados.

Si tienes activa la aplicación de Docker, detenla con CTRL+C o con docker compose down y vuelve la a ejecutar de nuevo:

$docker compose up

Luego abre la siguiente dirección en tu navegador https://educaproject.com/. Deberías ver un mensaje de advertencia similar al siguiente:

An invalid certificate warning



Esta pantalla puede variar dependiendo de tu navegador. Te alerta de que tu sitio no está utilizando un certificado confiable o válido; el navegador no puede verificar la identidad de tu sitio. Esto se debe a que firmaste tu propio certificado en lugar de obtener uno de una CA confiable. Cuando tienes un dominio real, puedes solicitar a una CA confiable que emita un certificado SSL/TLS para él, de modo que los navegadores puedan verificar su identidad. Si quieres obtener un certificado confiable para un dominio real, puedes referirte al proyecto Let's Encrypt creado por la Fundación Linux. Es una CA sin fines de lucro que simplifica la obtención y renovación de certificados SSL/TLS confiables de forma gratuita. Puedes encontrar más información en https://letsencrypt.org.

Haz clic en el enlace o botón que proporciona información adicional y elige visitar el sitio web, ignorando las advertencias. El navegador podría pedirte que añadas una excepción para este certificado o que verifiques que confías en él. Si estás usando Chrome, puede que no veas ninguna opción para ir al sitio web. Si este es el caso, escribe "thisisunsafe" y presiona Enter directamente en Chrome en la página de advertencia. Chrome entonces cargará el sitio web. Nota que haces esto con tu propio certificado emitido; no confíes en ningún certificado desconocido ni omitas las verificaciones de certificados SSL/TLS del navegador para otros dominios.

Cuando accedas al sitio, el navegador mostrará un ícono de candado junto a la URL.

The browser address bar, including a warning message

Si hace clic en el icono de candado o en el icono de advertencia, los detalles del certificado SSL/TLS se mostrarán los detalles del certificado.

En los detalles del certificado, verás que es un certificado autofirmado y podrás ver su fecha de expiración. Tu navegador podría marcar el certificado como no seguro, pero lo estás utilizando solo con fines de prueba. Ahora estás sirviendo tu sitio de manera segura a través de HTTPS.

Redirigiendo el tráfico HTTP a HTTPS


Estás redirigiendo solicitudes HTTP a HTTPS con Django usando la configuración SECURE_SSL_REDIRECT. Cualquier solicitud utilizando http:// se redirige a la misma URL usando https://. Sin embargo, esto se puede manejar de una manera más eficiente utilizando NGINX.

Edita el archivo config/nginx/default.conf.template y añade las siguientes líneas resaltadas en azul:

default.conf.template

# upstream for uWSGI
upstream uwsgi_app {
server unix:/code/educa/uwsgi_app.sock;
}

server {
listen 80;
server_name www.educaproject.com educaproject.com;
return 301 https://$host$request_uri;
}

server {
listen 443 ssl;
ssl_certificate /code/educa/ssl/educa.crt;
ssl_certificate_key /code/educa/ssl/educa.key;
server_name www.educaproject.com educaproject.com;
#...

En este código, eliminas la directiva listen 80; del bloque de servidor original, de modo que la plataforma esté disponible solo a través de HTTPS (puerto 443). Encima del bloque de servidor original, añades un bloque de servidor adicional que solo escucha en el puerto 80 y redirige todas las solicitudes HTTP a HTTPS. Para lograr esto, devuelves un código de respuesta HTTP 301 (redirección permanente) que redirige a la versión https:// de la URL solicitada utilizando las variables $host y $request_uri.

Abre una terminal en el directorio padre, donde se encuentra el archivo docker-compose.yml, y ejecuta el siguiente comando para recargar NGINX:

```
docker compose exec nginx nginx -s reload
```
Esto ejecuta el comando nginx -s reload en el contenedor de nginx. Ahora estás redirigiendo todo el tráfico HTTP a HTTPS utilizando NGINX.

Si abres el navegador y vas a http://www.educaproject.com verás como automáticamente nginx te redirige a htpps://www.educaproject.com.

Tu entorno ahora está asegurado con TLS/SSL. Para completar el entorno de producción, necesitas configurar un servidor web asíncrono para Django Channels.


Usando Daphne para Django Channels


Si has usado el programa de ejemplo que puse al principio del post, ya usamos Django Channels para construir un servidor de chat usando WebSockets. uWSGI es adecuado para ejecutar Django o cualquier otra aplicación WSGI, pero no admite comunicación asincrónica usando Asynchronous Server Gateway Interface (ASGI) o WebSockets. Para ejecutar Channels en producción, necesitas un servidor web ASGI que sea capaz de gestionar WebSockets. 

Daphne es un servidor HTTP, HTTP2 y WebSocket para ASGI desarrollado para servir Channels. Puedes ejecutar Daphne junto con uWSGI para servir aplicaciones ASGI y WSGI de manera eficiente. Puedes encontrar más información sobre Daphne en https://github.com/django/daphne.

Ya añadímos `daphne==4.1.2` al archivo `requirements.txt` del proyecto. Vamos a crear un nuevo servicio en el archivo `docker-compose.yml` para ejecutar el servidor web Daphne.

Edita el archivo `docker-compose.yml` y añade al final las siguientes líneas:

docker-compose.yml

daphne:
    build: .
    working_dir: /code/educa/
    command: ["../wait-for-it.sh", "db:5432", "--",
              "daphne", "-u", "/code/educa/daphne.sock",
              "educa.asgi:application"]
    restart: always
    volumes:
      - .:/code
    environment:
      - DJANGO_SETTINGS_MODULE=educa.settings.prod
      - POSTGRES_DB=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    depends_on:
      - db
      - cache
    user: "miusuario"

IMPORTANTE. Al añadir este contenedor al proyecto y tratar de ejecutar "docker compose up" me dio el siguiente problema de permisos:

failed to solve: error from sender: open Final/data/db: permission denied

Una solución sería cambiar los permisos y otra es detener los contenedores y las imágenes, eliminar ambos, y volver a realizar la composición con "docker compose up" además de borrar el archivo ./data con "rm -rf data". Para que todo funcione hay que volver a realizar las migraciones y crear un superusuario.

docker compose exec web python /code/educa/manage.py migrate
docker compose exec web python /code/educa/manage.py createsuperuser

Sigamos con la explicación.

La definición del servicio daphne es muy similar al servicio web. La imagen para el servicio daphne también se construye con el Dockerfile que creamos anteriormente para el servicio web. Las principales diferencias son:

- working_dir cambia el directorio de trabajo de la imagen a /code/educa/.

- command ejecuta la aplicación educa.asgi:application definida en el archivo educa/asgi.py con daphne utilizando un socket UNIX. También usa el script Bash wait-for-it para esperar a que la base de datos PostgreSQL esté lista antes de iniciar el servidor web.

Dado que estás ejecutando Django en producción, Django verifica los ALLOWED_HOSTS al recibir solicitudes HTTP. Implementaremos la misma validación para las conexiones WebSocket. Edita el archivo educa/asgi.py de tu proyecto y agrega las siguientes líneas destacadas en azul:

Final/educa/educa/asgi.py

import os

from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from channels.auth import AuthMiddlewareStack
import chat.routing

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'educa.settings')

django_asgi_app = get_asgi_application()

application = ProtocolTypeRouter({
    'http': django_asgi_app,
    'websocket': AllowedHostsOriginValidator(
        AuthMiddlewareStack(
        URLRouter(chat.routing.websocket_urlpatterns)
    )
    ),
})
La configuración de channels está lista para la produción.


Usando conexiones seguras para los websockets.


Hemos configurado Nginx para que use conexiones seguras con SSL/TLS. Tenemos que cambiar las conexiones ws (WebSocket) por conexiones seguras wss (WebSocket secure), de la misma forma que cambiamos las conexiones HTTP y ahora se sirven bajo HTTPS.

Edita la plantilla chat/rooom.html de la aplicación chat y encuentra la siguiente línea en el bloque doomready:

const url = 'ws://' + window.location.host +

Reemplázala con esta otra:

const url = 'wss://' + window.location.host +

Usando wss en lugar de ws, nos estamos conectando explícitamente a un WebSocket seguro.


Incluyendo Daphne en la configuración de Nginx.


En tu configuración de producción, ejecutarás Daphne en un socket UNIX y usarás NGINX frente a él. NGINX pasará solicitudes a Daphne según la ruta solicitada. Expondrás a Daphne a NGINX a través de
una interfaz de socket UNIX, al igual que la configuración uWSGI.

Edita el archivo config/nginx/default.conf.template y haz que tenga el siguiente aspecto:

default.conf.template

# upstream for uWSGI
upstream uwsgi_app {
server unix:/code/educa/uwsgi_app.sock;
}

# upstream for Daphne
upstream daphne {
server unix:/code/educa/daphne.sock;
}

server {
listen 80;
server_name www.educaproject.com educaproject.com;
return 301 https://$host$request_uri;
}

server {
listen 443 ssl;
ssl_certificate /code/educa/ssl/educa.crt;
ssl_certificate_key /code/educa/ssl/educa.key;
server_name www.educaproject.com educaproject.com;
error_log stderr warn;
access_log /dev/stdout main;
location / {
include /etc/nginx/uwsgi_params;
uwsgi_pass uwsgi_app;
}
location /ws/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
proxy_pass http://daphne;
}
location /static/ {
alias /code/educa/static/;
}
location /media/ {
alias /code/educa/media/;
}
}
En esta configuración, se establece un nuevo upstream llamado daphne, el cual apunta a un socket UNIX creado por Daphne. En el bloque del servidor, se configura la ubicación /ws/ para reenviar las solicitudes a Daphne. Se utiliza la directiva proxy_pass para pasar las solicitudes a Daphne e incluyes algunas directivas de proxy adicionales. Con esta configuración, NGINX pasará cualquier solicitud de URL que comience con el prefijo /ws/ a Daphne y el resto a uWSGI, excepto los archivos ubicados en las rutas /static/ o /media/, los cuales serán servidos directamente por NGINX.

La configuración de producción que incluye Daphne ahora se ve así:

The production environment request/response cycle, including Daphne


NGINX se ejecuta frente a uWSGI y Daphne como un servidor proxy inverso. NGINX se enfrenta a la web y pasa las solicitudes al servidor de aplicaciones (uWSGI o Daphne) en función de su prefijo de ruta. Además de esto, NGINX también sirve archivos estáticos y redirige solicitudes no seguras a solicitudes seguras. Esta configuración reduce el tiempo de inactividad, consume menos recursos del servidor y proporciona un mayor rendimiento y seguridad.

Detén la aplicación Docker desde la terminal presionando las teclas Ctrl + C o usando el botón de detener en la aplicación Docker Desktop. Luego, inicia Compose nuevamente con el comando:

```
docker compose up
```

Usa tu navegador para crear un curso de muestra con un usuario instructor, inicia sesión con un usuario que esté inscrito en el curso y abre https://educaproject.com/chat/room/1/ con tu navegador. Deberías poder enviar y recibir mensajes como en el siguiente ejemplo:

GRAFICO DE DAPNE Y NEGINX


Daphne está funcionando correctamente y NGINX está pasando las solicitudes de WebSocket a ella. Todas las conexiones están aseguradas con SSL/TLS.

Con esto hemos terminado. Has construido una pila personalizada lista para producción utilizando NGINX, uWSGI y Daphne. Podrías realizar más optimizaciones para mejorar el rendimiento y la seguridad mediante configuraciones adicionales en NGINX, uWSGI y Daphne. Sin embargo, esta configuración de producción es un excelente comienzo.

Has utilizado Docker Compose para definir y ejecutar servicios en múltiples contenedores. Ten en cuenta que puedes usar Docker Compose tanto para entornos de desarrollo local como para entornos de producción. Puedes encontrar información adicional sobre el uso de Docker Compose en producción en https://docs.docker.com/compose/production/.

Para entornos de producción más avanzados, necesitarás distribuir dinámicamente los contenedores entre un número variable de máquinas. Para eso, en lugar de Docker Compose, necesitarás un orquestador como Docker Swarm mode o Kubernetes. Puedes encontrar información sobre Docker Swarm mode en https://docs.docker.com/engine/swarm/ y sobre Kubernetes en https://kubernetes.io/docs/home/.

Crear un middleware personalizado


Ya conoces la configuración MIDDLEWARE, que contiene los middlewares para tu proyecto. Puedes pensar en ella como un sistema de plugins a bajo nivel, que te permite implementar ganchos que se ejecutan en el proceso de solicitud/respuesta. Cada middleware es responsable de una acción específica que se ejecutará para todas las solicitudes o respuestas HTTP.

Evita agregar procesamiento costoso a los middlewares, ya que se ejecutan en cada solicitud individualmente. Cuando se recibe una solicitud HTTP, los middlewares se ejecutan en el orden en que aparecen en la configuración MIDDLEWARE. Cuando Django genera una respuesta HTTP, esta pasa a través de todos los middlewares en orden inverso.

Un middleware puede escribirse como una función, de la siguiente manera:

def my_middleware(get_response):
    def middleware(request):
        # Código ejecutado para cada petición antes
        # de que se llame a la vista.
        response = get_response(request)
        # El código se ejecuta para cada petición/respuesta
        # después de que se llame a la vista.
        return response
    return middleware
Una fábrica de middleware es un callable que toma un callable get_response y devuelve un middleware. Un middleware es un callable que toma una solicitud y devuelve una respuesta, al igual que una vista. El callable get_response podría ser el siguiente middleware en la cadena o la vista actual en el caso del último middleware listado.

Si cualquier middleware devuelve una respuesta sin llamar a su callable get_response, interrumpe el proceso; no se ejecuta ningún otro middleware (tampoco la vista), y la respuesta se devuelve a través de las mismas capas por las que pasó la solicitud.

El orden de los middlewares en la configuración MIDDLEWARE es muy importante porque un middleware puede depender de los datos establecidos en la solicitud por otro middleware que se haya ejecutado previamente.

Cuando agregues un nuevo middleware a la configuración MIDDLEWARE, asegúrate de colocarlo en la posición correcta. Los middlewares se ejecutan en el orden en que aparecen en la configuración durante la fase de solicitud y en orden inverso para las respuestas.

Puedes encontrar más información sobre middlewares en https://docs.djangoproject.com/en/4.1/topics/http/middleware/
https://docs.djangoproject.com/en/4.1/topics/http/middleware/

Crear un middleware para subdominios

Vas a crear un middleware personalizado para permitir que los cursos sean accesibles a través de un subdominio personalizado. Cada URL de detalle de curso, que se ve como https://educaproject.com/course/django/, también será accesible a través del subdominio que utiliza el slug del curso, como https://django.educaproject.com/. Los usuarios podrán usar el subdominio como un atajo para acceder a los detalles del curso. Cualquier solicitud a subdominios será redirigida a la URL correspondiente del detalle del curso.

El middleware puede ubicarse en cualquier lugar dentro de tu proyecto. Sin embargo, se recomienda crear un archivo middleware.py en el directorio de tu aplicación.

Crea un nuevo archivo dentro del directorio de la aplicación courses y llámalo middleware.py. Agrega el siguiente código:

middleware.py

from django.urls import reverse
from django.shortcuts import get_object_or_404, redirect
from .models import Course


def subdomain_course_middleware(get_response):
    """
    Subdominio para los cursos.
    """
    def middleware(request):
        host_parts = request.get_host().split('.')
        if len(host_parts) > 2 and host_parts[0] != 'www':
            # get course for the given subdomain
            course = get_object_or_404(Course, slug=host_parts[0])
            course_url = reverse('course_detail',
                                 args=[course.slug])
            # redirect current request to the course_detail view
            url = '{}://{}{}'.format(request.scheme,
                                     '.'.join(host_parts[1:]), course_url)
            return redirect(url)
        response = get_response(request)
        return response
    return middleware

Cuando se recibe una petición HTML, se realizan las siguientes tareas:

1.- Obtenemos el nombre del host que se está usando en la petición y la dividimos en dos partes. Por ejemplo si el usuario accede a micurso.educaproject.com se genera la siguiente lista:

['micurso', 'educaproject', 'com']

2.- Comprobamos si el nombre del host incluye un subdominio comprobando si la lista tiene más de dos elementos. Si el nombre del host incluye un subdominio, y no es www, intentaremos acceder al curso con el slug proporcionado en el subdominio.

3.- Si no se encuentra el curso, elevaremos una excepción HTTP 404. Si se encuentra, redireccionamos el navegador a la url especifica del curso.

Edita el archivo settings/base.py del proyecto y añade 'courses.middleware.SubdomainCourseMiddleware' al final de toda la lista de Middleware, de la siguiente forma:

MIDDLEWARE = [
# ...
'courses.middleware.subdomain_course_middleware',
]
El "middkeware" se ejecutará ahora en cada petición.

Recuerda que los nombres de host permitidos para servir tu proyecto de Django están especificados en en la configuración ALLOWED_HOST. Vamos a modificarlo para permitir que cualquier posible subdominio del principal funcione.
 
Edita el archivo educa/settings/prod.py y modifica la configuración ALLOWED_HOST 

ALLOWED_HOSTS = ['.educaproject.com']

Un valor que comienza con un punto se utiliza como un comodín para los subdominios. ".educaproject.com" funcionará con educaproject.com y con cualquiera de sus subdominios, por ejemplo, course.educaproject.com y django.educaproject.com.


Sirviendo múltiples subdominios con Nginx.

Necesitamos que Nginx sirva nuestro sitio con cualquier posible subdominio. Para ello editaremos el archivo de configuración de Nginx "config/nginx/default.conf.template" y reemplazaremos esta línea:

server_name www.educaproject.com educaproject.com;

con esta otra:

server_name *.educaproject.com educaproject.com;

Al usar el asterisco, esta regla se aplicaría a todos los subdominios de educaproject.com. Para poder testear el middleware de forma local tendríamos que añadir esta linea al archivo etc/hosts/

127.0.0.1 django.educaproject.com

Si está en ejecución detén la aplicación de Docker desde el shell y luego vuelve a ejecutar docker compose de nuevo con:

docker compose up

Luego abre la dirección https://django.educaproject.com/ in tu navegador. El middleware encontrará el curso bajo el subdominio y te redirigirá a la dirección https://educaproject.com/course/django/.


Implementar comandos del usuario personalizados.

Django permite que tus aplicaciones registren comandos de gestión personalizados para la utilidad manage.py.

Un comando de gestión consiste en un módulo de Python que contiene una clase Command que hereda de django.core.management.base.BaseCommand o de una de sus subclases. Puedes crear comandos simples o hacer que acepten argumentos posicionales y opcionales como entrada.

Django busca comandos de gestión en el directorio management/commands/ de cada aplicación activa en la configuración INSTALLED_APPS. Cada módulo encontrado se registra como un comando de gestión con el mismo nombre.

Puedes aprender más sobre comandos de gestión personalizados en https://docs.djangoproject.com/
es/5.0/howto/custom-management-commands/.

Vas a crear un comando de gestión personalizado para recordar a los estudiantes que se inscriban en al menos un curso. El comando enviará un recordatorio por correo electrónico a los usuarios que han estado registrados por más tiempo que un período especificado y que aún no están inscritos en ningún curso.

Crea la siguiente estructura de archivos dentro del directorio de la aplicación students:
management/
    __init__.py
    commands/
        __init__.py
        enroll_reminder.py

Edita el archivo enroll_reminder.py y añade el siguiente código:

enroll_reminder.py

import datetime
from django.conf import settings
from django.core.management.base import BaseCommand
from django.core.mail import send_mass_mail
from django.contrib.auth.models import User
from django.db.models import Count
from django.utils import timezone


class Command(BaseCommand):
    help = 'Envia un e-mail para recordar a los usuarios registrados más de N dias \
          y que no estén aun registrados en algún curso aún'

    def add_arguments(self, parser):
        parser.add_argument('--days', dest='days', type=int)

    def handle(self, *args, **options):
        emails = []
        subject = 'Apúntate a un curso'
        date_joined = timezone.now().today() - \
            datetime.timedelta(days=options['days'] or 0)
        users = User.objects.annotate(course_count=Count('courses_joined')).filter(
            course_count=0, date_joined__date__lte=date_joined)
        for user in users:
            message = """Estimado {},
            Hemos visto que aún no te has apuntado a ningún curso.
            ¿A qué estas esperando?""".format(user.first_name)
            emails.append(
                (subject, message, settings.DEFAULT_FROM_EMAIL, [user.email]))
        send_mass_mail(emails)
        self.stdout.write('Enviar {} recordatorios'.format(len(emails)))

Este es tu comando enroll_reminder. El código anterior es el siguiente:

- La clase Command hereda de BaseCommand.

- Incluyes un atributo help. Este atributo proporciona una breve descripción del comando que se imprime si ejecutas el comando python manage.py help enroll_reminder.

- Utilizas el método add_arguments() para agregar el argumento nombrado --days. Este argumento se usa para especificar el número mínimo de días que un usuario debe estar registrado, sin haberse inscrito en ningún curso, para recibir el recordatorio.

- El comando handle() contiene el comando real. Obtienes el atributo days analizado desde la línea de comandos. Si esto no está configurado, utilizas 0, de modo que se envía un recordatorio a todos los usuarios que no se han inscrito en un curso, independientemente de cuándo se registraron. Utilizas la utilidad timezone proporcionada por Django para obtener la fecha actual con conocimiento de zona horaria con timezone.now().date(). (Puedes configurar la zona horaria para tu proyecto con la configuración TIME_ZONE). Recuperas a los usuarios que han estado registrados por más días de los especificados y que aún no están inscritos en ningún curso. Logras esto anotando el QuerySet con el número total de cursos en los que está inscrito cada usuario. Generas el correo electrónico de recordatorio para cada usuario y lo añades a la lista emails. Finalmente, envías los correos electrónicos utilizando la función send_mass_mail(), que está optimizada para abrir una sola conexión SMTP para enviar todos los correos electrónicos, en lugar de abrir una conexión por cada correo enviado.

Has creado tu primer comando de gestión. Abre la consola y ejecuta el comando:

docker compose exec web python /code/educa/manage.py \
enroll_reminder --days=20 --settings=educa.settings.prod

Si no tienes un servidor SMTP local en funcionamiento, puedes agregar la siguiente configuración al archivo settings.py para que Django muestre los correos electrónicos en la salida estándar durante el desarrollo:

```
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
```

Django también incluye una utilidad para llamar a comandos de gestión usando Python. Puedes ejecutar comandos de gestión desde tu código de la siguiente manera:

```
from django.core import management
management.call_command('enroll_reminder', days=20)
```