Fernando Mendes


Template Django com Docker, Postgres, Nginx e Certbot - Parte #1

Template Django com Docker, Postgres, Nginx e Certbot - Parte #1


Author:

fernandomendes

Created at:

20 de Agosto de 2024 às 11:08

Category

tutoriais


Implantar um aplicativo Django utilizando Docker com certificação HTTPS em um servidor na nuvem.


Uma das motivações para escrever este artigo foi a dificuldade que encontrei ao buscar materiais em português para implantar um aplicativo Django utilizando Docker com certificação HTTPS em um servidor na nuvem. Implantar essa estrutura de modo “tradicional” é relativamente simples, mas realizar o mesmo processo usando Docker não é . Após pesquisar e testar várias abordagens, encontrei estes dois artigos artigo 1, artigo 2, que serviram como referência para o template que criei.

Antes de prosseguir, vou supor que o leitor já tenha familiaridade com Docker, Nginx e Certbot, pois o foco será em como combinar esses serviços, e não em explicar seu funcionamento.

A estratégia é dividir o processo em três etapas. A primeira é o desenvolvimento do aplicativo dentro de um container Docker, juntamente com um container Postgres, que foi o SGBD escolhido. A segunda etapa é instalar o certificado Let’s Encrypt nos volumes correspondentes a cada serviço, Nginx e Certbot. A terceira e última etapa é rodar o serviço completo, com certificação HTTPS, e configurar um agendador de tarefas para manter o certificado válido automaticamente. Sem mais delongas, vamos lá.

Rodar um aplicativo Django com Docker é relativamente simples, e essa primeira parte pode ser implementada de várias maneiras diferentes. Vou apresentar uma forma, mas sinta-se livre para adaptar conforme necessário.

Crie um diretório de trabalho:

mkdir work_dir

cd work_dir

Crie um ambiente virtual Python e ative-o na pasta de trabalho:

python3 -m venv venv

source venv/bin/activate

Instale o Django e as dependências do projeto:

pip install --upgrade pip

pip install django gunicorn "psycopg[binary]"

Dentro do diretório de trabalho, crie uma pasta chamada app:

mkdir app

Ainda no diretório de trabalho, inicie um aplicativo Django e direcione a instalação para dentro da pasta app:

django-admin startproject appDjango app/

Crie um arquivo requirements.txt dentro da pasta app:

pip freeze > app/requirements.txt

Neste ponto, a estrutura do projeto deve estar assim:


work_dir
├── venv/
└── app/
    ├── manage.py
    ├── requirements.txt
    └── appDjango/
        ├── __init__.py
        ├── asgi.py
        ├── settings.py
        ├── urls.py
        └── wsgi.py

A partir de agora, implementaremos os containers do nosso app Django e Postgres, para que possamos iniciar o desenvolvimento do aplicativo. Para isso, é necessário ter o Docker e o Compose instalados em sua máquina. Há diversos tutoriais de instalação, além do próprio site do Docker. Se não tiver instalado, pare aqui, instale, e então continue com o processo.

O app.dockerfile ficará da seguinte forma:

FROM python:3.11.12-bullseye

# Evita criação de .pyc e força output direto no terminal
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Cria e define diretório de trabalho
WORKDIR /app

COPY app/requirements.txt /app/requirements.txt
COPY app /app
COPY scripts /scripts
# Instala libs do sistema
RUN apt-get update && apt-get -y dist-upgrade && \
    apt-get install -y libpq5 && \
    apt-get install -y netcat
RUN python -m venv /venv && \
    /venv/bin/pip install --upgrade pip && \
    /venv/bin/pip install -r /app/requirements.txt
RUN mkdir -p /web/static /web/media && \
    chmod -R 755 /venv /web/static /web/media && \
    chmod -R +x /scripts

# Define PATH para incluir o virtualenv e os scripts
ENV PATH="/app:/scripts:/venv/bin:$PATH"

# Comando de entrada
CMD ["/scripts/commands.sh"]

Você deve estar se perguntando: por que atualizar e instalar pacotes em um container Python? Quando estava desenvolvendo este template, me deparei com um erro no plugin do ORM do Django, relacionado à biblioteca psycopg. A psycopg3, que é utilizada diretamente pelo ORM do Django para conectar ao Postgres, usa uma biblioteca C chamada libpq5 por baixo dos panos, que costuma estar presente nos pacotes básicos do Ubuntu e Debian, mas não vem nos containers Python, tanto no Alpine quanto no Bullseye. Como já havia enfrentado esse erro anteriormente, a solução foi instalar o pacote libpq5, o que resolveu o problema. Além disso, a instalação de netcat é necessária para rodar o utilitário nc, que verifica se o host Postgres já está respondendo a solicitações, evitando que o app Django inicie sem o banco de dados, o que causaria um crash total do aplicativo. No entanto, já vi outros desenvolvedores usarem o Postgres em uma configuração semelhante e não enfrentarem esse problema.

Criando scripts para gerenciar o início do container:

O próximo passo é criar os scripts que irão gerenciar o início do nosso container. Optei por subdividi-los em um arquivo por comando, que podem ser estendidos conforme a necessidade. Durante o desenvolvimento, usaremos vários comandos dentro dos containers, então esses scripts serão muito úteis.

Crie um diretório scripts no diretório de trabalho:

mkdir scripts

cd scripts

Dentro do diretório scripts, crie os seguintes arquivos:

waitPSQL.sh

#!/bin/sh
while ! nc -z $POSTGRES_HOST $POSTGRES_PORT; do

  echo "🟡 Waiting for Postgres Database Startup ($POSTGRES_HOST $POSTGRES_PORT) ..."

  sleep 2
done
echo "✅ Postgres Database Started Successfully ($POSTGRES_HOST:$POSTGRES_PORT)"

collectstatic.sh

#!/bin/sh

python manage.py collectstatic --noinput

makemigrations.sh

#!/bin/sh

python manage.py makemigrations --noinput

migrate.sh

#!/bin/sh

makemigrations.sh
python manage.py migrate --noinput

runserver.sh

#!/bin/sh

# Este comando deve ser usado no desenvolvimento
python manage.py runserver 0.0.0.0:8000

# Este comando deve ser usado em produção
# gunicorn --config gunicorn_config.py Nome_Aplicacao.wsgi:application

commands.sh

#!/bin/sh

# O shell irá encerrar a execução do script quando um comando falhar
set -e
waitPSQL.sh
collectstatic.sh
migrate.sh
runserver.sh

Preparando o arquivo .env:

Para iniciar o nosso aplicativo em desenvolvimento, precisamos passar algumas informações para que os containers iniciem nosso app corretamente. Para isso, vamos criar o arquivo .env.

No diretório de trabalho, crie uma pasta chamada env_files:

mkdir env_files

cd env_files

No diretório env_files, crie um arquivo .env e cole o código abaixo:

SECRET_KEY="Chave_Secreta_Django" # (evite o caractere $)
DEBUG="1" # 1 para desenvolvimento, 0 para produção.
CSRF_TRUSTED_ORIGINS="https://DOMAIN"
ALLOWED_HOSTS="DOMAIN, 0.0.0.0, 127.0.0.1, [::1], localhost"
DB_ENGINE="django.db.backends.postgresql" # Para banco de dados PostgreSQL
POSTGRES_DB="Nome_do_Banco"
POSTGRES_USER="Usuario"
POSTGRES_PASSWORD="Senha_Forte"
POSTGRES_HOST="psql"
POSTGRES_PORT="5432"
POSTGRES_HOST_AUTH_METHOD="trust"

Este arquivo substitui alguns parâmetros no arquivo settings.py. Ele deve ser mantido fora dos commits, pois contém informações sensíveis de segurança, portanto, adicione-o ao arquivo .gitignore. Para manter este artigo objetivo, vou abordar somente o necessário.

Nosso settings.py deve estar parecido com esse:


from pathlib import Path
import os

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent

# Local dos arquivos estáticos -> /data/web/static
# Local dos arquivos de mídia -> /data/web/media
DATA_DIR = BASE_DIR.parent / 'web'

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY', 'OK')

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = bool(int(os.getenv('DEBUG', 0)))

ALLOWED_HOSTS = [
    h.strip() for h in os.getenv('ALLOWED_HOSTS', '').split(',') if h.strip()
]

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'hello_django.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'hello_django.wsgi.application'

# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': os.getenv('DB_ENGINE', 'django.db.backends.postgresql'),
        'NAME': os.getenv('POSTGRES_DB', 'CHANGE'),
        'USER': os.getenv('POSTGRES_USER', 'CHANGE'),
        'PASSWORD': os.getenv('POSTGRES_PASSWORD', 'CHANGE'),
        'HOST': os.getenv('POSTGRES_HOST', 'psql'),
        'PORT': os.getenv('POSTGRES_PORT', '5432'),
    }
}

# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/

LANGUAGE_CODE = 'pt-br'

TIME_ZONE = 'America/Sao_Paulo'

USE_I18N = True

USE_TZ = True

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/

STATICFILES_DIRS = [
    BASE_DIR / 'templates' / 'static',
]

STATIC_URL = '/static/'
STATIC_ROOT = DATA_DIR / 'static'

MEDIA_URL = '/media/'
MEDIA_ROOT = DATA_DIR / 'media'

# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

Criando o arquivo docker-compose.yml:

Agora vamos criar um arquivo Compose para rodar nossos containers do app Django e Postgres. A estrutura básica está assim:

No diretório de trabalho, crie um arquivo docker-compose.yml:

version: "3.9"

services:
  web:
    build:
      context: .
      dockerfile: app.dockerfile
    command: commands.sh
    env_file:
      - env_files/.env
    volumes:
      - web_static:/web/static
      - web_media:/web/media
    depends_on:
      - psql
    ports:
      - 8000:8000
    networks:
      - internal-network

  psql:
    image: postgres:15.2-alpine
    env_file:
      - env_files/.env
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    ports:
      - 5432:5432
    networks:
      - internal-network

networks:
   internal-network:
     driver: bridge

volumes:
  postgres_data:
  web_static:
  web_media:

Neste ponto, nosso diretório de trabalho deve estar assim:

work_dir
├── venv/
├── app/
│   ├── app_django/
│   │   ├── __init__.py
│   │   ├── asgi.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   ├── manage.py
│   └── requirements.txt
├── data/
│   └── postgres/
│       └── data/
├── env_files/
│   └── .env
├── scripts/
│   ├── commands.sh
│   ├── collectstatic.sh
│   ├── makemigrations.sh
│   ├── migrate.sh
│   ├── runserver.sh
│   └── waitPSQL.sh
├── web/
│   ├── media/
│   └── static/
├── app.dockerfile
└── docker-compose-dev.yml

Agora vamos construir o nosso container, levantar os serviços e acessar a aplicação no navegador.

No diretório de trabalho, execute os seguintes comandos:

docker compose build

docker compose up

Após a criação dos containers, será possível acessar o navegador utilizando:

localhost:8000

127.0.0.1:8000

Se tudo foi feito corretamente, você verá a página inicial do Django e já poderá começar a desenvolver seu aplicativo. Qualquer problema ou dúvida, sugiro revisar os passos. Se encontrar algo errado, faça uma pesquisa para verificar se houve alguma mudança no Docker, Postgres, Django ou Python, pois estes estão em constante evolução.

Na segunda parte deste artigo, abordaremos a criação do arquivo Nginx para o Compose, a criação dos volumes correspondentes, a instalação do Certbot e a finalização da configuração para permitir acesso ao aplicativo de forma segura, com certificação HTTPS.

Para acessar o repositório no GitHub é só clicar Aqui.

Até a próxima!