Django y PostgreSQL orquestado con docker compose

schedule   domingo, 08 septiembre 2024

Hablaremos sobre cómo arrancar un proyecto Django conectado a un PostgreSQL desde cero, estando los dos servicios separados con su propio contenedor y orquestados con docker compose.

Lo más habitual suele ser que nos unamos a un proyecto ya iniciado, en el que únicamente tengamos que clonarnos el repositorio, construir la imagen (espero que uséis, vosotros o vuestra empresa, Docker o cualquier otro servicio de empaquetado de aplicaciones, sino comenzamos mal 😤), y levantar los servicios necesarios para el proyecto. Pero en ciertas ocasiones, tendremos la necesidad de tener que construir el proyecto desde cero.

Es por ello, por lo que he decidido realizar este post para guiaros punto por punto en la implementación de una aplicación Django conectada a un PostgreSQL, estando ambos servicios orquestados con docker compose.

Tabla de contenido

Dicho esto… code time! 🪄✨

Crear un entorno virtual Python

Antes de nada, nos creamos el directorio donde alojaremos nuestro proyecto.

mkdir dj-pg-docker && cd dj-pg-docker

En mi caso tengo la versión de Python 3.12.5 disponible en mi sistema por haberla instalado previamente con pyenv. Para más información sobre como instalar una versión en concreto, visitar la documentación en su proyecto de GitHub.

Dicho esto, creamos un entorno virtual y lo activamos.

python -m venv venv
source venv/bin/activate

Con el primer comando habremos creado un directorio en la raíz de nuestro proyecto conteniendo el entorno virtual, y con el segundo lo habremos activado. El prompt de la shell debe haber cambiado indicando al principio el nombre del entorno virtual activo. Algo parecido a esto:

(venv) user@machine:~/projects/dj-pg-docker$

Instalar Django, crear proyecto base y aplicar las migraciones

Tener la última versión de Django nos brinda tener todas sus novedades, pero (la vida está llena de peros 🥲) yo soy partidario de usar siempre la última versión LTS, por eso de tener varios años cubiertos de soporte oficial. En el momento de escribir este post, la última versión LTS disponible es la 4.2, con lo que procederemos a instalarla en nuestro entorno virtual usando el gestor de paquetes Python; pip.

pip install Django==4.2

Una vez instalado Django, procedemos a crear el proyecto base.

django-admin startproject my_project

Deberíamos tener la estructura del proyecto similar a como se muestra a continuación.

dj-pg-docker
├─── my_project/
│    ├─── manage.py
│    └─── my_project/
│        ├─── asgi.py
│        ├─── __init__.py
│        ├─── settings.py
│        ├─── urls.py
│        └─── wsgi.py
└─── venv

Para asegurarnos de que todo funciona correctamente, accedemos al directorio del proyecto, aplicamos las migraciones iniciales, y arrancamos el server.

cd my_project
python manage.py migrate
python manage.py runserver

Deberíamos ver la pantalla principal de nuestro proyecto accediendo a http://localhost:8000.

It works! 🚀 Paramos el servicio con Ctrl + C y seguimos.

Preparar la estructura de directorios

Como vamos a usar Docker y docker compose para construir la imagen de nuestra app y levantar los servicios, hay ciertos directorios que ya no nos harán falta.

Desactivamos el entorno virtual y lo eliminamos.

deactivate
rm -r venv

Dentro del proyecto base creado por Django, en el directorio my_project, eliminaremos la bbdd SQLite (se trata del gestor de bbdd que establece Django de forma predeterminada, más adelante lo cambiaremos) que se creó cuando aplicamos las migraciones.

rm my_project/db.sqlite3

Y en el mismo directorio aprovechamos para crear el fichero requirements.txt, donde añadiremos la misma versión indicada cuando lo instalamos dentro del entorno virtual.

touch my_project/requirements.txt
echo "Django==4.2" > my_project/requirements.txt

Para crear el fichero requirements.txt también podríamos haber usado la instrucción pip freeze > my_project/requirements.txt teniendo activo el entorno virtual, lo que sucede es que de esa manera también se añaden dependencias extras que se instalan de forma indirecta al instalar Django (u otros paquetes). Yo particularmente, prefiero especificar las dependencias de librerías que necesito exactamente, y las sub dependencias que se encargue pip de instalarlas. De esta manera tendremos un fichero de dependencias mucho más limpio.

Por último, renombramos el directorio del proyecto base a app. Esto no influye en nada, está claro, pero así lo dejamos más limpio cara a dockerizarlo.

mv my_project app

Estructura del proyecto tras los cambios realizados.

dj-pg-docker
└─── app/
     ├─── manage.py
     ├─── my_project
     │   ├─── asgi.py
     │   ├─── __init__.py
     │   ├─── settings.py
     │   ├─── urls.py
     │   └─── wsgi.py
     └─── requirements.txt

Definir la imagen para nuestra app Django en un Dockerfile

El directorio app será donde alojaremos todos los subdirectorios y ficheros para nuestro proyecto Django. Con lo que dentro de dicho directorio crearemos la estructura de carpetas compose/local:

mkdir -p app/compose/local

Y dentro del directorio local añadiremos los ficheros Dockerfile, entrypoint y start con el siguiente contenido.

# dj-pg-docker/app/compose/local/Dockerfile

# Imagen oficial Python
ARG PYTHON_VERSION=3.12.5-slim-bullseye

FROM python:${PYTHON_VERSION} as python

FROM python as builder

# Dependencias del sistema
RUN apt-get update && apt-get install -y \
    build-essential \
    # Dependencias librería psycopg2 (conector a PosgreSQL)
    libpq-dev

# Dependencias de Python
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip
RUN pip wheel --wheel-dir /usr/src/app/wheels -r requirements.txt

FROM python as executor

# Variables de entorno
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1

# Dependencias del sistema
RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev \
    # Para comprobar el estado de la bbdd
    netcat \
    # Limpieza de ficheros innecesarios
    && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
    && rm -rf /var/lib/apt/lists/*

# Copiamos las dependencias de Python desde el builder
COPY --from=builder /usr/src/app/wheels  /wheels/

# Y las instalamos
RUN pip install --no-cache-dir --upgrade pip
RUN pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* \
    && rm -rf /wheels/

# Copiamos el entrypoint y scripts
COPY ./compose/local/entrypoint /entrypoint
COPY ./compose/local/start /start

# Y les damos permisos de ejecución
RUN sed -i 's/\r$//g' /entrypoint \
    && sed -i 's/\r$//g' /start \
    && chmod +x /entrypoint \
    && chmod +x /start

WORKDIR /app

ENTRYPOINT [ "/entrypoint" ]
# dj-pg-docker/app/compose/local/entrypoint

#!/bin/sh

if [ "$DATABASE" = "postgres" ]
then
    echo "Waiting for postgres..."

    while ! nc -z $SQL_HOST $SQL_PORT; do
      sleep 0.1
    done

    echo "PostgreSQL started"
fi

exec "$@"
# dj-pg-docker/app/compose/local/start

#!/bin/bash

python manage.py migrate
python manage.py runserver 0.0.0.0:8000

Wow! Parece mucha info así de golpe, pero vamos a explicar la finalidad de cada fichero para ver qué implican exactamente.

Antes de seguir, aseguraros que tengáis la estructura del proyecto tal que así:

dj-pg-docker
└─── app/
     ├─── compose
     │    └─── local
     │         ├─── Dockerfile
     │         ├─── entrypoint
     │         └─── start
     ├─── manage.py
     ├─── my_project
     │   ├─── asgi.py
     │   ├─── __init__.py
     │   ├─── settings.py
     │   ├─── urls.py
     │   └─── wsgi.py
     └─── requirements.txt

Definir los servicios en el fichero docker-compose.yml

Turno de que definamos los servicios en el fichero docker-compose.yml. Vamos allá.

Creamos el fichero en la raíz del proyecto, en dj-pg-docker, con el siguiente contenido:

# dj-pg-docker/docker-compose.yml

services:

  web:
    image: dj-pg-docker:latest
    build:
      context: app/
      dockerfile: ./compose/local/Dockerfile
    command: /start
    volumes:
      - ./app:/app
    ports:
      - 8000:8000
    env_file:
      - ./.env
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    environment:
      - POSTGRES_USER=${SQL_USER}
      - POSTGRES_PASSWORD=${SQL_PASSWORD}
      - POSTGRES_DB=${SQL_DATABASE}

volumes:
  postgres_data:

Hemos definido dos servicios, uno para nuestra app Django que se encarga de usar nuestro Dockerfile para construir la imagen, y otro para PostgreSQL, que en este caso usamos la imagen oficial publicada en dockerhub.

Cosas que debemos tener en cuenta:

Dado que hemos hablado varias veces del fichero .env, vamos a crearlo en nuestra raíz.

touch .env

Y añadimos las siguientes variables.

# dj-pg-docker/.env

# Django
DEBUG=1
SECRET_KEY=super_secure_django_secret_key
DJANGO_ALLOWED_HOSTS="localhost 127.0.0.1 [::1]"

# PostgreSQL
SQL_USER=pythoninspanish
SQL_PASSWORD=pythoninspanish
SQL_DATABASE=pythoninspanish
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres

Usar las variables de entorno es más que recomendable, ya que evitamos meter usuarios o credenciales en nuestro código directamente, con las vulnerabilidades de seguridad que conllevaría. En entornos de producción usaríamos secrets que nos brinden las propias plataformas que usemos para desplegar nuestro proyecto, pero eso ya se sale del foco de este post.

Para el SECRET_KEY que usa Django, podemos usar el módulo secrets de Python para generar una key aleatoria.

import secrets
secrets.token_urlsafe(64)

Establecer a vuestro gusto los valores del usuario, contraseña y base de datos de PostgreSQL.

Para terminar de configurar nuestro proyecto, nos quedaría usar ciertas variables de entorno en nuestro settings.py, vamos allá.

# dj-pg-docker/app/my_project/settings.py

...

import os

...

SECRET_KEY = os.environ.get("SECRET_KEY")
DEBUG = bool(os.environ.get("DEBUG", 0))
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split()

...

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.getenv("SQL_DATABASE"),
        "USER": os.getenv("SQL_USER"),
        "PASSWORD": os.getenv("SQL_PASSWORD"),
        "HOST": os.getenv("SQL_HOST"),
        "PORT": os.getenv("SQL_PORT"),
    }
}

...

A parte de usar los valores definidos en nuestro fichero con las variables de entorno (.env), lo cual nos permite gestionar diferentes parámetros de forma dinámica, lo más reseñable es la configuración para conectarnos a la base de datos. En este caso le estamos indicando a Django que debe conectarse al PostgreSQL indicado, pero para ello necesitará una librería para poder realizar dicha conexión. Añadimos el paquete al fichero requirements.txt.

echo "psycopg2==2.9.9" >> app/requirements.txt

Quedando nuestro fichero de dependencias de la siguiente manera.

# dj-pg-docker/app/requirements.txt

Django==4.2
psycopg2==2.9.9

Nos aseguramos que tengamos la estructura del proyecto como se muestra a continuación antes de construir y levantar los servicios.

dj-pg-docker
├─── app/
│    ├─── compose
│    │    └─── local
│    │         ├─── Dockerfile
│    │         ├─── entrypoint
│    │         └─── start
│    ├─── manage.py
│    ├─── my_project
│    │   ├─── asgi.py
│    │   ├─── __init__.py
│    │   ├─── settings.py
│    │   ├─── urls.py
│    │   └─── wsgi.py
│    └─── requirements.txt
├─── docker-compose.yml
└─── .env

Construir la imagen y levantar los servicios

Primero construimos las imágenes necesarias en función de los servicios definidos en nuestro docker-compose.yml. En nuestro caso, únicamente se nos creará la imagen para el servicio app, el cual usará nuestro Dockerfile. El servicio db al usar una imagen publicada en dockerhub no hará nada, simplemente se la bajará cuando posteriormente levantemos los servicios. Dicho esto, construyamos nuestra imagen.

docker compose build

Una vez termine, nuestra imagen debería aparecer en el listado de las imágenes que tengamos en nuestra máquina.

docker images | grep dj-pg-docker

Mostrando algo parecido a esto.

dj-pg-docker          latest          8b930233872f   1 minute ago   412MB

Por último, levantamos los dos servicios.

docker compose up

Si no tenéis la imagen de PostgreSQL descargada en vuestra máquina, el proceso tardará un poco hasta que se descargue la imagen de dockerhub. Una vez descargada, se inicializarán ambos servicios.

Deberíais poder acceder de nuevo a la instancia yendo a http://localhost:8000.

Podéis parar los servicios con Ctrl + C.

Llegados a este punto tendremos dos servicios, uno de Django y otro de PostgreSQL, ambos corriendo en contenedores independientes y orquestados con docker compose 🚀.

Siguientes pasos

Ahora que ya tenemos la base del proyecto configurada con docker compose, existen varios puntos que se podrían mejorar/implementar. Veámoslos:


Espero os sea de utilidad este post. Hasta la próxima, ¡pythonistas! 🐍❤️

Ir al inicio