Docker resulta útil a la hora de crear un entorno de desarrollo ya que empaqueta la aplicación en contenedores con lo necesario para que pueda funcionar sin importar el sistema operativo que corre. Incluso si no tienes dependencias o servicios, como Postgres instalados en tu máquina.
Este tutorial quiero enseñarte cómo puedes tener tu primer contenedor con una aplicación de Django. Ojo, estoy suponiendo que tienes Pipenv, Docker y Docker Compose.
Lo primero que debemos hacer es crear un proyecto con Django con su entorno virtual.
mkdir PlatziDocker
pipenv install --three django
pipenv run django-admin startproject config .
Listo. Si Pipenv no te resulta familiar, escribí un tutorial acerca de esta herramienta hace un par de semanas, te puede ahorra tiempo a la hora de usar entornos virtuales.
Sigamos, ahora es necesario crear un Dockerfile. Así, sin extensión ni nada parecido.
Nuestro Dockerfile se debe ver así.
FROM python:3.6ENV PYTHONUNBUFFERED 1COPY. /code/
WORKDIR/code/
RUNpip install pipenv
RUNpipenv install --system
EXPOSE8000
La primera línea FROM python:3.6
le dice a Docker que vamos a usar esa imagen como base para construir la nuestra. Es un contenedor de Ubuntu que tiene instalado la versión 3.6 de Python.
La segunda línea ENV PYTHONUNBUFFERED 1
es una variable de entorno diciéndole a Docker que nos muestre el Standart Output(salida) y Standart Error(errores) en la terminal como estamos acostumbrados. De esta misma manera podemos declarar un ENV SECRET_KEY='__KEY__'
para tener nuestra llave secreta alejada de nuestro archivo settings.
La tercera línea COPY . /code/
es un comando para copiar todos los archivos y carpetas que se encuentran al mismo nivel (.) del Dockerfile a una carpeta llamada code. Luego en WORKDIR /code/
le decimos que nuestra área de trabajo va a ser esa carpeta, todos los comandos que corramos, se ejecutarán dentro de la misma.
Las siguientes dos líneas RUN pip install pipenv
y RUN pipenv install --system
le dirán a docker que ejecute esos comandos, primero instalará Pipenv con pip para luego ejecutar la siguiente e instalar las dependencias de nuestro archivo Pipfile.lock
y como le estamos pasando como argumento --system
no se va a crear un entorno virtual, no lo necesitamos.
Y la última línea EXPOSE 8000
, para correr bien runserver
nuestro contenedor necesita acceder al puerto 8000.
Listo, con eso ya tenemos nuestro Dockerfile. Ahora solo necesitamos hacer docker build .
y vamos a ver una larga lista de cosas en la terminal, diferentes pasos. Todo debería ir bien.
Ahora, Docker Compose nos ayuda a correr más de un contenedor en nuestra aplicación de Docker, por ejemplo si queremos tener una base de datos como Postgres o un servicio de caché como Redis o un servidor como Nginx. Para esto necesitamos un archivo en el mismo nivel que nuestro archivo Dockerfile
y lo llamamos docker-compose.yml
, este si tiene una extensión. Es un archivo YAML.
Tendría esto:
versiónversion: '3'
services: db: image: postgres:10.1 volumes:
- postgres_data:/var/lib/postgresql/data
web: build: .
command: python /code/manage.py migrate --noinput
command: python /code/manage.py runserver 0.0.0.0:8000 volumes:
- .:/code
ports:
- "8000:8000" depends_on:
- db
environment:
- DJANGO_SETTINGS_MODULE=config.settings
- SECRET_KEY=${SECRET_KEY}
volumes:postgres_data:
Ahora vamos a explicarlo línea por línea.
La primera version: '3'
define la versión de Compose que queremos usar.
Ahora en services
vamos a definir, de manera identada, los servicios que queremos corran en diferentes contenedores.
Con el primer servicio le decimos a docker que queremos la imagen de PostgreSQL. Ni siquiera necesitamos tenerlo instalado en nuestra sistema, Docker la descargará desde el Docker Hub con la versión que le especificamos. Y con volumes
le decimos en que parte de nuestro contenedor queremos que se guarden los datos, en este caso /var/lib/postgresql/data
. Si recuerdan, les dije que nuestra imagen de Python tiene como base una imagen de Ubuntu, eso hace que tenga varios subdirectorios. Un volumen permite que nuestros datos persistan sin importar el ciclo de vida del contenedor
Por supuesto, para esto tenemos que decirle a Django que vamos a usar Postgres como base de datos, así que tenemos que deshacernos de ese sqlite3. Colocaremos esto:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'postgres',
'USER': 'postgres',
'PASSWORD': 'postgres',
'HOST': 'db',
'PORT': '5432',
}
}
El siguiente servicio es el web
y le decimos que construya build .
nuestra imagen desde el directorio actual. Los siguientes dos commandos command
, uno ejecutará las migraciones sin mostrar la salida en nuestra consola --no-input
y el segundo levantará el servidor cuando corramos el contenedor.
volumes
al igual que con la base de datos, le dira a nuestro contenedor donde estará nuestro código ./code
. Justo después con ports
mapeamos nuestro puerto 8000 al puerto 8000 del contenedor de Docker, haciendo que nuestro servidor sea accesible vía localhost:8000 o 0.0.0.0:8000. Ahora depends_on
le dice que nuestro servicio web necesita esperar por la base de datos, Compose primero levantará ese servicio antes de pasar a web.
Con environment
, puedes adivinar que son variables de entorno. Si es el caso de que estamos trabajando con diferentes archivos settings. Puedes colocar ahí, la variable DJANGO_SETTINGS_MODULE
para especificar con cuál archivo quieres trabajar, puede ser local, production o test. La siguiente SECRET_KEY
es una manera de llamar a una variable de entorno colocada en un archivo .env
Ya que Pipenv carga automáticamente las variables de un archivo .env
podemos usar ese mismo archivo para mandarle la variable de entorno a nuestro contenedor con la línea SECRET_KEY=${SECRET_KEY}
Y nuestras últimas dos líneas, Compose tiene una regla donde tenemos que listar nuestros volúmenes en una llave volumes
al mismo nivel de identación que services
o version
Con el archivo guardado. Podemos ejecutar ahora docker-compose up
también nos mostrará cierta cantidad de información en la terminal pero al final deberías tener un servidor corriendo en localhost:8000 o 0.0.0.0:8000 con la acostumbrada salida de Django.
De esta manera, puedes olvidarte de los entornos virtuales y de tener algunos programas instalados en tu máquina local, puedes tener tu aplicación empaquetada de esta manera y usarla en cualquier SO mientras tengas Docker instalado. Para saber más acerca de Docker te invito a ver los dos grandes cursos que tiene Platzi, aprenderás esto y más.
Demasiados errores y omisiones, es imposible realizar con éxito este ejemplo siguiendo las indicaciones, ya que como indicó tienen errores.
Creo que deben mejorar en el control de calidad de lo que se publica, ya que afecta negativamente el esfuerzo por aprender nuevas técnologías de software.
Buen tutorial y una buena forma de unir dos cursos dentro de la plataforma, llego aquí 3 años después y aun es relevante el contenido
Super bien explicado
Hola comunidad en Platzi que utiliza Django.
Tengo una pregunta muy importante en mis proyectos de Django con Docker, ya que seguido tengo este error y no entiendo porqué.
La razón es por que aveces me aparece y aveces no.
docker-compose up Starting premiosplatziapp_db_1 ... done Starting premiosplatziapp_web_1 ... done Attaching to premiosplatziapp_db_1, premiosplatziapp_web_1 db_1 | db_1 | PostgreSQL Database directory appears to contain a database; Skipping initialization db_1 | db_1 | 2022-02-20 04:36:28.055 UTC [1] LOG: starting PostgreSQL 14.2 (Debian 14.2-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit db_1 | 2022-02-20 04:36:28.056 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432 db_1 | 2022-02-20 04:36:28.057 UTC [1] LOG: listening on IPv6 address "::", port 5432 db_1 | 2022-02-20 04:36:28.064 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" db_1 | 2022-02-20 04:36:28.082 UTC [30] LOG: database system was interrupted; last known up at 2022-02-20 04:34:04 UTC web_1 | Watching forfile changes with StatReloader web_1 | Performing system checks... web_1 | web_1 | System check identified no issues (0 silenced). db_1 | 2022-02-20 04:36:31.093 UTC [31] FATAL: the database system is starting up web_1 | Exception in thread django-main-thread: web_1 | Traceback (most recent call last): web_1 | File"/usr/local/lib/python3.8/site-packages/django/db/backends/base/base.py", line 230, in ensure_connection web_1 | self.connect() web_1 | File"/usr/local/lib/python3.8/site-packages/django/utils/asyncio.py", line 25, in inner web_1 | return func(*args, **kwargs) web_1 | File"/usr/local/lib/python3.8/site-packages/django/db/backends/base/base.py", line 211, in connect web_1 | self.connection = self.get_new_connection(conn_params) web_1 | File"/usr/local/lib/python3.8/site-packages/django/utils/asyncio.py", line 25, in inner web_1 | return func(*args, **kwargs) web_1 | File"/usr/local/lib/python3.8/site-packages/django/db/backends/postgresql/base.py", line 199, in get_new_connection web_1 | connection = Database.connect(**conn_params) web_1 | File"/usr/local/lib/python3.8/site-packages/psycopg2/__init__.py", line 122, in connect web_1 | conn = _connect(dsn, connection_factory=connection_factory, **kwasync) web_1 | psycopg2.OperationalError: connection to server at "db" (172.29.0.2), port 5432 failed: FATAL: the database system is starting up web_1 | web_1 | web_1 | The above exception was the direct cause of the following exception: web_1 | web_1 | Traceback (most recent call last): web_1 | File"/usr/local/lib/python3.8/threading.py", line 932, in _bootstrap_inner web_1 | self.run() web_1 | File"/usr/local/lib/python3.8/threading.py", line 870, inrun web_1 | self._target(*self._args, **self._kwargs) web_1 | File"/usr/local/lib/python3.8/site-packages/django/utils/autoreload.py", line 64, in wrapper web_1 | fn(*args, **kwargs) web_1 | File"/usr/local/lib/python3.8/site-packages/django/core/management/commands/runserver.py", line 127, in inner_run web_1 | self.check_migrations() web_1 | File"/usr/local/lib/python3.8/site-packages/django/core/management/base.py", line 505, in check_migrations web_1 | executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS]) web_1 | File"/usr/local/lib/python3.8/site-packages/django/db/migrations/executor.py", line 18, in __init__ web_1 | self.loader = MigrationLoader(self.connection) web_1 | File"/usr/local/lib/python3.8/site-packages/django/db/migrations/loader.py", line 53, in __init__ web_1 | self.build_graph() web_1 | File"/usr/local/lib/python3.8/site-packages/django/db/migrations/loader.py", line 223, in build_graph web_1 | self.applied_migrations = recorder.applied_migrations() web_1 | File"/usr/local/lib/python3.8/site-packages/django/db/migrations/recorder.py", line 77, in applied_migrations web_1 | if self.has_table(): web_1 | File"/usr/local/lib/python3.8/site-packages/django/db/migrations/recorder.py", line 55, in has_table web_1 | with self.connection.cursor() as cursor: web_1 | File"/usr/local/lib/python3.8/site-packages/django/utils/asyncio.py", line 25, in inner web_1 | return func(*args, **kwargs) web_1 | File"/usr/local/lib/python3.8/site-packages/django/db/backends/base/base.py", line 270, in cursor web_1 | return self._cursor() web_1 | File"/usr/local/lib/python3.8/site-packages/django/db/backends/base/base.py", line 246, in _cursor web_1 | self.ensure_connection() web_1 | File"/usr/local/lib/python3.8/site-packages/django/utils/asyncio.py", line 25, in inner web_1 | return func(*args, **kwargs) web_1 | File"/usr/local/lib/python3.8/site-packages/django/db/backends/base/base.py", line 230, in ensure_connection web_1 | self.connect() web_1 | File"/usr/local/lib/python3.8/site-packages/django/db/utils.py", line 90, in __exit__ web_1 | raise dj_exc_value.with_traceback(traceback) from exc_value web_1 | File"/usr/local/lib/python3.8/site-packages/django/db/backends/base/base.py", line 230, in ensure_connection web_1 | self.connect() web_1 | File"/usr/local/lib/python3.8/site-packages/django/utils/asyncio.py", line 25, in inner web_1 | return func(*args, **kwargs) web_1 | File"/usr/local/lib/python3.8/site-packages/django/db/backends/base/base.py", line 211, in connect web_1 | self.connection = self.get_new_connection(conn_params) web_1 | File"/usr/local/lib/python3.8/site-packages/django/utils/asyncio.py", line 25, in inner web_1 | return func(*args, **kwargs) web_1 | File"/usr/local/lib/python3.8/site-packages/django/db/backends/postgresql/base.py", line 199, in get_new_connection web_1 | connection = Database.connect(**conn_params) web_1 | File"/usr/local/lib/python3.8/site-packages/psycopg2/__init__.py", line 122, in connect web_1 | conn = _connect(dsn, connection_factory=connection_factory, **kwasync) web_1 | django.db.utils.OperationalError: connection to server at "db" (172.29.0.2), port 5432 failed: FATAL: the database system is starting up web_1 | db_1 | 2022-02-20 04:36:31.291 UTC [30] LOG: database system was not properly shut down; automatic recovery in progress db_1 | 2022-02-20 04:36:31.337 UTC [30] LOG: redo starts at 0/1869138 db_1 | 2022-02-20 04:36:31.337 UTC [30] LOG: invalid record length at 0/1869170: wanted 24, got 0 db_1 | 2022-02-20 04:36:31.337 UTC [30] LOG: redo done at 0/1869138 system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s db_1 | 2022-02-20 04:36:31.372 UTC [1] LOG: database system is ready to accept connections ^CGracefully stopping... (press Ctrl+C again to force) Killing premiosplatziapp_web_1 ... done Killing premiosplatziapp_db_1 ... done ERROR: 2 MacBook@MacBook-Pro ~/D/P/P/premiosplatziapp (deployment) [2]> docker-compose build
Docker compose
version:"3.9" services: db: image: postgres volumes: - ./data/db:/var/lib/postgresql/data environment: - POSTGRES_NAME=postgres - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres web: build: . command: python manage.py runserver 0.0.0.0:8000 volumes: - .:/code ports: - "8000:8000" depends_on: - db
Dockerfile
FROM python:3.8ENV PYTHONUNBUFFERED=1WORKDIR/code COPYrequirements.txt /code/ RUNpip install -r requirements.txt COPY. /code/
Pienso que el error se debe a que no encuentra la Base de Datos en ocasiones, es por eso que lo tengo que detener y volver a hacer rebuild cada rato hasta que funciona.
Gracias.
Ejecutado:
MacBook@MacBook-Pro ~/D/P/P/premiosplatziapp (deployment)> docker-compose up Starting premiosplatziapp_db_1 ... done Recreating premiosplatziapp_web_1 ... done Attaching to premiosplatziapp_db_1, premiosplatziapp_web_1 db_1 | db_1 | PostgreSQL Database directory appears to contain a database; Skipping initialization db_1 | db_1 | 2022-02-2004:37:24.551 UTC [1] LOG: starting PostgreSQL 14.2 (Debian 14.2-1.pgdg110+1) onx86_64-pc-linux-gnu, compiledbygcc (Debian10.2.1-6) 10.2.120210110, 64-bit db_1 | 2022-02-2004:37:24.551 UTC [1] LOG: listening onIPv4address"0.0.0.0", port5432 db_1 | 2022-02-2004:37:24.551 UTC [1] LOG: listening onIPv6address"::", port5432 db_1 | 2022-02-2004:37:24.568 UTC [1] LOG: listening onUnixsocket"/var/run/postgresql/.s.PGSQL.5432" db_1 | 2022-02-2004:37:24.597 UTC [27] LOG: database system was interrupted; last known up at2022-02-2004:36:31 UTC db_1 | 2022-02-2004:37:28.493 UTC [27] LOG: database system was not properly shut down; automatic recovery in progress db_1 | 2022-02-2004:37:28.501 UTC [27] LOG: redo starts at0/18691E8 db_1 | 2022-02-2004:37:28.501 UTC [27] LOG: invalid record lengthat0/1869220: wanted 24, got 0 db_1 | 2022-02-2004:37:28.501 UTC [27] LOG: redo done at0/18691E8system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s db_1 | 2022-02-2004:37:28.561 UTC [1] LOG: database system is ready to accept connections web_1 | Watching forfile changes with StatReloader web_1 | Performing system checks... web_1 | web_1 | System check identified no issues (0 silenced). web_1 | February 19, 2022 - 22:37:28 web_1 | Django version4.0, using settings 'premiosplatziapp.settings' web_1 | Starting development server athttp://0.0.0.0:8000/ web_1 | Quit the server with CONTROL-C. web_1 | [19/Feb/202222:37:46] "GET / HTTP/1.1"2001336