Django é um framework web de código aberto de alto nível escrito em Python que pode ajudá-lo a construir sua aplicação Python rapidamente. Ele incentiva o desenvolvimento rápido e um design limpo e pragmático, seguindo o padrão arquitetural model–template–views. Pronto para uso, o framework vem com os componentes de aplicação modernos necessários, como autenticação de usuário, framework de cache, mapeador objeto-relacional, despachador de URL, sistema de templates, e interface administrativa personalizável.
Gunicorn ‘Green Unicorn’ é um Servidor HTTP WSGI Python para sistemas UNIX. O servidor Gunicorn é compatível com vários frameworks web, oferece ótimo desempenho e é leve em recursos do servidor. Docker é uma plataforma de contêineres de código aberto que já existe há algum tempo, tornando o desenvolvimento de aplicações rápido, eficiente e previsível.
Neste tutorial, você adquirirá habilidades no desenvolvimento e implantação de aplicativos web Django conteinerizados escaláveis. Estaremos usando um aplicativo Django Polls criado seguindo os guias introdutórios de introdução ao Django. No momento da redação do tutorial, nós o baseamos no Django 3.2 suportado pelo Python 3.6 ou posterior. Implantaremos o aplicativo como um contêiner com o Docker e o serviremos com o servidor Gunicorn. Claro, antes de implantar o aplicativo Django em um contêiner, você terá que fazer algumas modificações no código do projeto para lidar com coisas como registro em fluxos de saída padrão (logging) e trabalho com variáveis de ambiente. Arquivos estáticos, como imagens CSS e JavaScript, podem ser descarregados para serviços de armazenamento de objetos para permitir o gerenciamento fácil dos arquivos a partir de um único local em um ambiente multi-contêiner.
Mostraremos como implementar essas modificações com base na bem delineada metodologia twelve-factor para a construção de aplicações web escaláveis. Depois de concluir as modificações, você criará uma imagem Docker da aplicação e implantará o aplicativo conteinerizado com o Docker. Recomendamos que você acompanhe as etapas descritas no tutorial para obter uma compreensão completa do tutorial.
Pré-requisitos
Sendo este um tutorial prático, incentivamos você a ter a configuração abaixo para ajudá-lo a acompanhar:
-
Um servidor Ubuntu 20.04. Você pode seguir as etapas de 1 a 4 deste tutorial passo a passo para ajudá-lo a configurar seu servidor Ubuntu na CloudSigma.
-
Certifique-se de adicionar um usuário com privilégios sudo em ambos os nós que usaremos para executar os comandos conforme descrito no tutorial acima.
-
Instale o Docker no servidor. Você pode seguir as etapas 1, 2 e 3 do nosso tutorial sobre instalação e operação do Docker. Lembre-se de adicionar o usuário sudo criado acima ao grupo Docker.
-
Um espaço de armazenamento de objetos compatível. O Django suporta vários serviços de armazenamento, conforme listado na documentação do django-storages. Você pode escolher o que preferir e seguir a documentação para configurá-lo. Para este tutorial, usaremos o MinIO que é um serviço de armazenamento em nuvem compatível com S3.
-
Uma instância de banco de dados SQL. O Django suporta vários bancos de dados SQL que você é livre para escolher. Para este tutorial, usaremos o PostgreSQL. O banco de dados PostgreSQL não será implantado dentro de um contêiner. Configuraremos um servidor Ubuntu separado para hospedar a instância do PostgreSQL para garantir que alcancemos nossa configuração multi-contêiner, bem como a persistência de dados. Você pode criar outra instância do Ubuntu 20.04 e seguir este tutorial para Configurar uma instância de banco de dados PostgreSQL no Ubuntu. Lembre-se de adicionar uma função no banco de dados PostgreSQL para o seu usuário sudo, conforme explicado nas Etapas 2 e 3. Esta função permitirá que você se conecte ao banco de dados a partir dos outros servidores que hospedam seus contêneres.
De acordo com esses pré-requisitos, você deve ter duas instâncias de servidor Ubuntu. Uma instância executará seu contêiner Docker e a outra instância executará a instância PostgreSQL. Vamos começar!
Etapa 1: Configurando a Instância de Banco de Dados PostgreSQL
Nesta seção, modificaremos as configurações do Postgres no servidor Ubuntu que executa a instância do Postgres. Isso permitirá conexões de um endereço IP externo. Assim que conectar, poderemos criar um banco de dados e uma função de usuário, específicos para o aplicativo Django Polls que estamos implantando.
Primeiro, se você configurou seu ambiente de acordo com os Pré-requisitos, você deve ter uma função em seu banco de dados PostgreSQL para seu usuário sudo. Em seguida, precisamos definir uma senha para essa função. Enquanto estiver no servidor que executa o PostgreSQL, faça login no terminal do Postgres com o seguinte comando:
|
1 |
sudo -u postgres psql |
Uma vez no terminal do Postgres, execute o comando \password para alterar a senha de um usuário. A sintaxe para o comando \password é \password <username>. Para o nosso caso, o comando:
|
1 |
\password cloudsigma |
Insira a senha e confirme-a. Salve essa senha em um local seguro, pois você a usará para se autenticar a partir do outro servidor Ubuntu mais tarde. Depois disso, digite exit e pressione Enter para sair do terminal do Postgres.
Se você ativou o firewall (ufw) na instância do servidor PostgreSQL, precisará permitir o tráfego para a porta padrão do Postgres 5432. Você pode restringir o tráfego para originar-se apenas de um endereço IP específico do seu outro servidor Ubuntu que executará o contêiner Docker. Execute o seguinte comando para adicionar a regra do ufw, substituindo o seu endereço IP onde destacado:
|
1 |
sudo ufw allow from ubuntu_server_ip_address to any port 5432 |
Isso garantirá que apenas o seu servidor possa se conectar à instância do PostgreSQL. Embora isso permita o tráfego através do firewall, você também precisa modificar os arquivos de configuração do PostgreSQL para permitir a conexão a partir do endereço IP remoto. Por padrão, a configuração permite apenas a conexão a partir do localhost. Os arquivos de configuração do PostgreSQL são encontrados no diretório /etc/postgresql/12/main. 12, neste caso, é a versão do PostgreSQL que instalamos para este tutorial. Você pode ter instalado uma versão diferente. Assim, você pode mudar para o diretório /etc/postgresql/ e listar o conteúdo para encontrar o número da versão do PostgreSQL que você instalou.
Use o nano para modificar o arquivo de configuração:
|
1 |
sudo nano /etc/postgresql/12/main/postgresql.conf |
Encontre a linha abaixo e remova o comentário, definindo-a para permitir conexões de todos os IPs:
|
1 |
listen_addresses = '*' |
Salve e feche o arquivo. Em seguida, você deve editar o arquivo pg_hba.conf também, ele está no mesmo diretório que o postgresql.conf. O pg_hba.conf permite definir a partir de quais computadores você pode se conectar à instância do PostgreSQL, bem como o método de autenticação. Abra o arquivo com o nano:
|
1 |
sudo nano /etc/postgresql/12/main/pg_hba.conf |
Por favor, leia os comentários neste arquivo para entender as palavras-chave. A seção que estamos procurando é esta:

Nosso foco será na segunda linha, você deseja que ela fique parecida com a linha abaixo após remover o comentário:
|
1 |
host all all your_ubuntu_server_ip/24 md5 |
Por favor, substitua a parte destacada pelo endereço IP do seu servidor Ubuntu para permitir que ele se conecte à instância do PostgreSQL. Salve o arquivo assim que estiver pronto. Reinicie o banco de dados PostgreSQL para que as alterações entrem em vigor:
|
1 |
sudo service postgresql restart |
Nosso outro servidor Ubuntu com o endereço IP especificado deve ser capaz de se conectar à instância do Postgres.
Passo 2: Conectando à Instância do Servidor PostgreSQL e Criando um Banco de Dados e Usuário
Neste passo, tentaremos garantir que a instância do Ubuntu que serve nosso contêiner Docker possa se conectar ao outro servidor que executa a instância do PostgreSQL. Faça login na instância do Ubuntu que possui o Docker e instale o pacote postgresql-client dentro da máquina hospedeira Ubuntu (ainda não dentro do contêiner).
Como de costume, primeiro atualize o pacote apt e depois instale o pacote com os seguintes comandos:
|
1 |
sudo apt update |
|
1 |
sudo apt install postgresql-client |
O pacote instalado acima ajudará você a criar um banco de dados e um usuário para sua aplicação. Em seguida, precisamos nos conectar à instância do PostgreSQL fornecendo os parâmetros de conexão ao cliente PostgreSQL.
Os parâmetros de conexão seguem esta sintaxe:
|
1 |
psql -U username -h host -p port -d database --set=sslmode=require |
Neste comando, o username é o usuário/função que você adicionou ao seu banco de dados PostgreSQL. host é o endereço IP da instância do Ubuntu que executa o seu banco de dados PostgreSQL. port é a porta padrão na qual o Postgres escuta conexões de entrada, ou seja, 5432. No lugar do database, usaremos o banco de dados padrão chamado postgres que vem com a instalação do PostgreSQL. Substitua seus valores nas partes destacadas adequadamente e pressione Enter. Quando solicitado, insira a senha que você definiu. Isso fará o login no prompt do Postgres, onde você poderá gerenciar o banco de dados.
Você se conectou com sucesso à instância do PostgreSQL. Agora você pode criar um banco de dados para o aplicativo de enquetes do Django. Vamos chamá-lo de django_polls:
|
1 |
CREATE DATABASE django_polls; |
Certifique-se de que sua instrução termine com um ponto e vírgula para evitar erros. Em seguida, mude para o banco de dados django_polls com o comando:
|
1 |
\c django_polls; |
Em seguida, crie um usuário de banco de dados específico para este projeto. Vamos nomear o usuário django_user:
|
1 |
CREATE USER django_user WITH PASSWORD 'password'; |
Escolha uma senha segura para o seu usuário. Feito isso, precisamos modificar os parâmetros de conexão para o usuário que acabamos de criar. Isso ajuda a acelerar as operações do banco de dados, garantindo que os valores corretos não sejam consultados e definidos cada vez que uma conexão for estabelecida.
Defina a codificação padrão que o Django espera como UTF-8:
|
1 |
ALTER ROLE django_user SET client_encoding TO 'utf8'; |
Em seguida, defina o esquema de isolamento de transação padrão para “ read committed”, o que bloqueia leituras de transações não confirmadas:
|
1 |
ALTER ROLE django_user SET default_transaction_isolation TO 'read committed'; |
Defina seu fuso horário. Para manter o tutorial universal, usaremos o UTC:
|
1 |
ALTER ROLE django_user SET timezone TO 'UTC'; |
Por fim, conceda privilégios administrativos do banco de dados ao novo usuário:
|
1 |
GRANT ALL PRIVILEGES ON DATABASE django_polls TO django_user; |
Saia do prompt do PostgreSQL quando estiver pronto:
|
1 |
\q |
Isso é tudo para esta etapa. Depois de configurar corretamente seu aplicativo Django, ele deverá ser capaz de gerenciar seu banco de dados.
Etapa 3: Baixando o aplicativo de um repositório Git e definindo as dependências
Nesta etapa, clonaremos o repositório do aplicativo Django-polls. Este repositório contém o código para o Django’s tutorial de escrita do seu primeiro aplicativo Django.
Faça login no servidor Ubuntu que executa o Docker, crie um diretório chamado django_project e navegue até ele:
|
1 2 |
mkdir django_project cd django_project |
Em seguida, clone o repositório no diretório com o seguinte comando:
|
1 |
git clone https://github.com/jaymoh/django-polls.git |
Navegue até o diretório e liste o conteúdo:
|
1 |
cd django-polls |
Liste o conteúdo do diretório:
|
1 |
ls |

Observe os seguintes itens:
-
manage.py: este arquivo é a entrada para o utilitário de linha de comando que o Django fornece para gerenciar seu aplicativo.
-
mysite: um diretório com o escopo do projeto Django e as configurações de código.
-
polls: um diretório contendo o código do aplicativo polls .
-
templates: contém arquivos de modelo personalizados para as páginas de administração.
Para saber mais sobre como realmente criamos o projeto, dê uma olhada no Writing your first Django app da documentação oficial. Dentro do django-polls diretório, queremos ter nossas dependências Python definidas em um arquivo de texto. Nós o chamaremos de requirements.txt. Abra o arquivo com seu editor de preferência:
|
1 |
nano requirements.txt |
Cole as seguintes linhas dentro do arquivo para declarar as dependências:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Django==3.2.9 gunicorn==20.1.0 docutils==0.18.1 sqlparse==0.4.2 jmespath==0.10.0 psycopg2==2.9.2 python-dateutil==2.8.2 pytz==2021.3 six==1.16.0 urllib3==1.26.7 django-storages==1.12.2 minio==7.1.6 django-minio-backend==3.3.2 django-dotenv==1.4.2 boto3==1.21.38 |
Neste arquivo, definimos as dependências Python com suas versões exatas que devem ser instaladas quando você compilar o aplicativo. Algumas delas incluem Django, django-storages para interagir com buckets de armazenamento de objetos, o psycopg2 adaptador para PostgreSQL, o gunicorn servidor WSGI, e outras dependências adicionais. Salve e feche o arquivo quando terminar.
Passo 4: Configurando Variáveis de Ambiente para um Aplicativo Django
A twelve-factor app metodologia recomenda que você extraia configurações codificadas de forma rígida da base de código do seu aplicativo. Ao fazer isso, você ganha a liberdade de alterar o comportamento do aplicativo em tempo de execução modificando variáveis de ambiente sem tocar na base de código. O Docker funciona com essa configuração, por isso modificaremos o arquivo de configurações para funcionar com variáveis de ambiente. Kubernetes também funciona com essa estrutura de configuração. Compartilharemos outro tutorial sobre implantação com Kubernetes no blog da CloudSigma.
O settings.py é o principal arquivo de configurações de um projeto Django. É um módulo Python que usa estruturas de dados nativas para configurar o aplicativo. Para o nosso aplicativo, o arquivo está no local django-polls/mysite/settings.py. A maioria de seus valores é codificada de forma rígida. Isso exigirá que você modifique o arquivo de configuração na base de código se alterar o comportamento do aplicativo. Queremos mudar isso. Felizmente, o Python oferece a função getenv no módulo os . Podemos usá-la para configurar o Django para ler parâmetros de configuração a partir de variáveis de ambiente locais.
Vamos continuar modificando o arquivo django-polls/mysite/settings.py para substituir os valores codificados de forma rígida das variáveis que podemos querer atualizar em tempo de execução com uma chamada para os.getenv. Esta função lê o valor definido no nome da variável de ambiente fornecido. Opcionalmente, você pode fornecer um segundo parâmetro, que é um valor padrão que será usado se a variável de ambiente não estiver definida.
Aqui está um exemplo:
|
1 |
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') |
Na linha acima, dizemos ao Django para recuperar a chave secreta da variável de ambiente. Não fornecemos um valor alternativo, pois forneceremos a chave externamente. Se ela não existir, o aplicativo não deverá ser iniciado. Ao fornecer a chave secreta externamente, também queremos garantir que todas as cópias em contêiner do aplicativo estejam usando a mesma chave nos vários servidores. Isso evita possíveis problemas que surgem quando as várias cópias do aplicativo usam chaves diferentes.
Aqui está outro exemplo com uma opção padrão:
|
1 |
DEBUG = os.getenv('DEBUG', False) |
Nesta linha, definimos uma variável de ambiente DEBUG que deve ser lida. No entanto, se ela não estiver definida, fornecemos um segundo parâmetro que será passado para a variável de configuração DEBUG. DEBUG é definida como False para garantir que informações confidenciais não sejam passadas para o frontend caso haja um problema com o aplicativo. No entanto, se estivermos em modo de desenvolvimento, queremos que ela seja definida como True para nos permitir ver as informações de erro para facilitar a correção de erros.
Agora que você sabe a importância das variáveis de ambiente, abra o arquivo django_project/django-polls/settings.py em seu editor. Primeiro, importe o módulo os adicionando esta linha no topo do arquivo settings.py:
|
1 |
import os |
Em seguida, encontre essas variáveis e atualize-as da seguinte forma:
|
1 2 3 |
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') DEBUG = os.getenv('DEBUG', False) ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '127.0.0.1').split(',') |
Na configuração ALLOWED_HOSTS, especificamos que ela deve obter o valor da DJANGO_ALLOWED_HOSTS variável de ambiente, e dividi-lo em uma lista Python usando vírgula ( ,) como separador. Se a variável estiver ausente, ALLOWED_HOSTS é definida como 127.0.0.1.
Em seguida, role pelo arquivo e encontre a seção DATABASES, configure-a para ler também das variáveis de ambiente:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.{}'.format( os.getenv('DB_ENGINE', 'sqlite3') ), 'NAME': os.getenv('DB_DATABASE', 'django_polls'), 'USER': os.getenv('DB_USERNAME', 'your_db_username'), 'PASSWORD': os.getenv('DB_PASSWORD', 'your_secure_default_password'), 'HOST': os.getenv('DB_HOST', '127.0.0.1'), 'PORT': os.getenv('DB_PORT', 5432), 'OPTIONS': json.loads( os.getenv('DB_OPTIONS', '{}') ), } } |
Observe que adicionamos o módulo json.loads. Você também deve adicionar uma importação do módulo no topo do arquivo settings.py :
|
1 |
import json |
A função json.loads desserializa um objeto JSON passado para DATABASES['default']['OPTIONS'] a partir da variável de ambiente DB_OPTIONS. Especificar essa opção nos permite passar uma estrutura de dados arbitrária para definir a configuração do banco de dados. Um mecanismo de banco de dados inclui um conjunto de opções válidas aplicáveis a ele. A opção JSON nos dá a flexibilidade de codificar um objeto JSON com os parâmetros apropriados para o mecanismo de banco de dados que estamos usando no momento.
A configuração DATABASES['default']['NAME'] especifica o nome do banco de dados no sistema de gerenciamento de banco de dados relacional que configuramos. No caso de usar um banco de dados SQLite, você deve especificar o caminho para o arquivo do banco de dados.
Observe que o Python oferece vários métodos para ler variáveis de ambiente externas. Usamos apenas um deles. Você é livre para pesquisar e usar outros métodos. Nesta etapa, você aprendeu a trabalhar com variáveis de ambiente externas. Isso lhe dá a flexibilidade de alterar as variáveis e modificar o comportamento do aplicativo em execução em contêineres. Na próxima etapa, você aprenderá a trabalhar com serviços de armazenamento de objetos.
Etapa 5: Trabalhando com Serviços de Armazenamento de Objetos Externos
Uma grande vantagem de conteinerizar sua aplicação é torná-la portátil para facilitar a implantação de várias cópias do aplicativo quando o tráfego aumentar. Assim, abrindo espaço para escalabilidade. No entanto, isso traz o problema de manter versões de arquivos estáticos e recursos em vários contêineres. Graças aos avanços na tecnologia de nuvem, você pode descarregar esses elementos estáticos compartilhados para um armazenamento externo. Em seguida, você pode tornar os arquivos acessíveis por meio de uma rede para todos os seus contêineres em execução. Em vez de tentar sincronizar os arquivos entre os vários contêineres em execução, você tem um local central para gerenciá-los.
O conceito que estamos tentando explicar acima é o uso de serviços de armazenamento de objetos em nuvem, ou Simple Storage Services (S3). O Django possui um pacote chamado django-storages que permite trabalhar com backends de armazenamento remoto. Django-storages funciona com a maioria dos serviços de armazenamento de objetos compatíveis com S3, como FTP, SFTP, AWS S3 da Amazon, Google Cloud Storage, Dropbox e Azure Storage, entre outros. Neste tutorial, usaremos o MinIO. Sinta-se à vontade para usar qualquer outro serviço de armazenamento de objetos compatível com S3. MinIO oferece armazenamento de objetos de alto desempenho e compatível com S3. Com o MinIO, você pode criar uma infraestrutura de dados compatível com S3 em qualquer nuvem.
Mostraremos como configurar um serviço de armazenamento MinIO na plataforma CloudSigma. Siga estas etapas:
-
Comece por criar uma conta na CloudSigma. Se você encontrar algum problema ao criar o armazenamento MinIO, entre em contato com o suporte por chat ao vivo gratuito 24/7 da CloudSigma’s, e eles o ajudarão.
-
Adicione suas informações de faturamento.
-
Em seguida, solicite seu bucket acessível publicamente aqui: https://blog.cloudsigma.com/xxxx. Você precisará entrar em contato com o suporte do Chat ao Vivo para obter as credenciais de acesso da sua conta.
-
Assim que o seu ambiente de armazenamento de objetos MinIO for criado, você receberá as credenciais de acesso e outras instruções para acessá-lo. As credenciais devem incluir o seu MINI_ACCESS_KEY, MINIO_SECRET_KEY, e MINIO_URL. Você usará essas chaves nas instruções abaixo.
Vamos fazer mais algumas alterações no arquivo mysite/settings.py que estivemos modificando na etapa anterior. No arquivo, adicione o app storages à lista de INSTALLED_APPS:

O storages app é instalado via django-storages conforme definido no requirements.txt. Role até o final do arquivo e substitua a variável STATIC_URL pelo seguinte trecho de código:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ DEFAULT_FILE_STORAGE = os.getenv('STATIC_DEFAULT_FILE_STORAGE', 'storages.backends.s3boto3.S3Boto3Storage') AWS_S3_ENDPOINT_URL = os.getenv('MINIO_URL') AWS_ACCESS_KEY_ID = os.getenv('MINIO_ACCESS_KEY') AWS_SECRET_ACCESS_KEY = os.getenv('MINIO_SECRET_KEY') AWS_STORAGE_BUCKET_NAME = os.getenv('STATIC_MINIO_BUCKET_NAME') AWS_S3_OBJECT_PARAMETERS = { 'CacheControl': 'max-age=86400', } AWS_LOCATION = 'static' AWS_DEFAULT_ACL = 'public-read' STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' STATIC_URL = '{}/{}/'.format(AWS_S3_ENDPOINT_URL, AWS_LOCATION) STATIC_ROOT = "static/" |
Observe que algumas das variáveis de configuração estão codificadas diretamente (hard-coded):
-
STATICFILES_STORAGE: define o backend de armazenamento que o Django usará para lidar com arquivos estáticos. Em nosso guia, estamos usando o armazenamento MinIO, mas você pode usar qualquer backend compatível com S3, conforme explicado na documentação do Django Storages.
-
AWS_S3_OBJECT_PARAMETERS: define os cabeçalhos de controle de cache (cache-control).
-
AWS_LOCATION: usamos isso para definir um diretório dentro do bucket de armazenamento onde todos os arquivos estáticos serão armazenados. Você pode escolher um nome diferente.
-
AWS_DEFAULT_ACL: define a lista de controle de acesso (ACL) para arquivos estáticos. Definir o valor como ‘ public-Read’ tornará os arquivos acessíveis a todos os usuários públicos.
-
STATIC_URL: O Django usa a URL base definida nesta variável para gerar URLs para arquivos estáticos. A URL base, neste caso, é obtida combinando a URL do endpoint e o subdiretório de arquivos estáticos.
-
STATIC_ROOT: define onde coletar arquivos estáticos localmente antes de copiá-los para o armazenamento de objetos remoto.
Também temos algumas variáveis de ambiente definidas externamente para manter a flexibilidade e a portabilidade:
-
AWS_STORAGE_BUCKET_NAME: define o nome do bucket de armazenamento para o qual o Django fará o upload dos assets.
-
AWS_S3_ENDPOINT_URL: define a URL do endpoint usada para acessar o serviço de armazenamento de objetos. Esta será a URL mapeada para o servidor que hospeda seu serviço MinIO.
Salve e feche o arquivo quando terminar de editar.
Depois de definir essas configurações e instalar as dependências declaradas do Python, você poderá executar o comando do Django manage.py collectstatic a qualquer momento para reunir os arquivos estáticos do seu projeto e enviá-los para o backend de armazenamento de objetos remoto:
|
1 |
python manage.py collectstatic |
No entanto, ainda não configuramos o arquivo env com as configurações, então provavelmente falhará.
Ao executar o comando, leva um momento para copiar seus assets para o MinIO Cloud Storage, dependendo do tamanho deles e da velocidade da sua internet.
Isso é tudo para esta etapa. Vamos ver como podemos lidar com o envio de logs do Django para o Docker Engine para que você possa visualizá-los usando o comando docker logs na próxima etapa.
Step 6: Setting up Logging in a Django App
No modo Debug, quando a opção DEBUG está definida como True, o Django registra informações na saída padrão e no erro padrão. As informações de log geralmente aparecem no terminal no qual você iniciou o servidor HTTP de desenvolvimento.
Em produção, você provavelmente estará usando um servidor HTTP diferente, e a opção DEBUG estará definida como False. O Django usará um método de logging diferente neste caso. O Django envia logs de prioridade ERROR ou CRITICAL para uma conta de e-mail administrativa que você definir. Isso funciona muito bem para muitas situações.
Em ambientes conteinerizados e Kubernetes, o logging para a saída padrão e erro padrão é altamente recomendado. As mensagens de log são coletadas em um único diretório no sistema de arquivos do Node e são facilmente acessíveis usando os comandos kubectl e docker . Com um ponto de logging centralizado no sistema de arquivos do Node, a equipe de operações pode facilmente executar processos em cada nó para monitorar e encaminhar logs. Portanto, devemos configurar nossa aplicação para gravar logs nessa estrutura padrão.
Você ficará feliz em saber que o Django aproveita o módulo logging altamente personalizável da biblioteca padrão do Python. Isso permite que você defina um dicionário que passa por logging.config.dictConfig para definir as saídas e a formatação desejadas. Aqui está um ótimo artigo sobre Django Logging, The Right Way que pode ajudá-lo a dominar técnicas de logging no Django.
Abra o arquivo django-polls/mysite/settings.py em seu editor. Adicione uma importação para a biblioteca logging.config do Python no topo do arquivo:
|
1 |
import logging.config |
Até agora, com todas as importações que adicionamos, sua seção de importações em settings.py deve se parecer com isto:

A biblioteca logging.config recebe um dicionário de nova configuração de logging por meio da função dictConfig para substituir o comportamento de logging padrão do Django.
Role até o final do arquivo e adicione o seguinte trecho de código de configuração de logging:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# Configuração de Logging # Desativar configuração anterior LOGGING_CONFIG = None # Obter loglevel do env LOGLEVEL = os.getenv('DJANGO_LOGLEVEL', 'info').upper() logging.config.dictConfig({ 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'console': { 'format': '%(asctime)s %(levelname)s [%(name)s:%(lineno)s] %(module)s %(process)d %(thread)d %(message)s', }, }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'formatter': 'console', }, }, 'loggers': { '': { 'level': LOGLEVEL, 'handlers': ['console',], }, }, }) |
LOGGING_CONFIG está definido como None para desativar/limpar as configurações de log padrão que o Django define. LOGLEVEL é definido pela DJANGO_LOGLEVEL variável de ambiente. No entanto, se ela não existir, queremos que seja definida como ‘ info’.
O módulo logging.config que importamos no topo fornece uma função dictConfig que é usada para definir um novo dicionário de configuração. O dicionário define a formatação de texto usando a chave formatters. A saída é definida com a chave handlers, e, finalmente, a chave loggers define qual mensagem deve ir para qual manipulador.
Depois de definir essas configurações, Docker exporá os logs através do comando docker logs. Da mesma forma, em outro tutorial que faremos para o Kubernetes, você poderá visualizar os logs com o comando kubectl logs. Vamos agora começar o processo de conteinerização na próxima etapa.
Passo 7: Definindo o Dockerfile da Aplicação
Nesta etapa, definimos a configuração para iniciar a imagem do contêiner que executará o aplicativo Django servido pelo servidor WSGI Gunicorn. Definiremos o ambiente de execução para construir uma imagem de contêiner, instalaremos o aplicativo e suas dependências e realizaremos algumas configurações finais.
-
A imagem pai para um aplicativo Django
Decidir a imagem base na qual basear seu contêiner é a primeiríssima decisão que você tomará ao lidar com implantações conteinerizadas. Claro, você tem a opção de construir suas imagens de contêiner do zero (SCRATCH), ou seja, um sistema de arquivos vazio, ou baseá-las em uma imagem de contêiner existente. Como não queremos reinventar a roda, construiremos nossa imagem a partir de uma imagem base. Existem muitas imagens de contêiner de código aberto disponíveis no repositório oficial de imagens de contêiner do Docker. A menos que você esteja construindo sua imagem do zero, é altamente recomendável usar uma imagem do hub oficial do Docker. Isso ocorre porque o Docker verifica as imagens para garantir que sigam as melhores práticas, além de garantir atualizações regulares e patches de segurança.
Como o Django é um framework Python, aproveitaremos uma imagem com um ambiente Python padrão que já possui as ferramentas e bibliotecas de que precisamos instaladas. Na página oficial de imagens Python no Docker Hub, você pode encontrar uma imagem baseada em Python para várias versões do Python.
A partir de nossos vários tutoriais baseados em Docker, você notará que usamos imagens baseadas no Alpine Linux. O Alpine Linux oferece um ambiente de sistema operacional robusto, porém enxuto, para executar aplicativos conteinerizados. Embora seu sistema de arquivos seja pequeno, ele é extensível e vem com um sistema de gerenciamento de pacotes completo, com a possibilidade de adicionar funcionalidades.
Ao escolher uma imagem base no Docker Hub, você pode notar várias tags disponíveis para cada imagem. Para o Python, temos 3-alpine, que aponta para a imagem da versão mais recente do Python 3 da versão mais recente do Alpine. Isso significa que, caso seu projeto funcione com uma versão de imagem mais antiga, ele pode quebrar quando os mantenedores da imagem Docker fizerem uma atualização. Para evitar tais cenários no futuro, é sempre recomendável escolher as tags mais específicas para a imagem que você deseja usar.
Neste tutorial, usaremos a 3.8.12-alpine3.15 imagem como a imagem base para a nossa aplicação Django. Esta tag específica será especificada no Dockerfile usando a FROM . O Dockerfile estará no diretório principal do projeto: django_project.
Comece navegando para fora do Django-polls de volta para o diretório django_project :
|
1 |
cd .. |
Uma vez no diretório, use seu editor favorito para abrir um arquivo chamado Dockerfile :
|
1 |
nano Dockerfile |
Em seguida, cole a seguinte linha para definir a base da sua imagem:
|
1 |
FROM python:3.8.12-alpine3.15 |
A FROM define o ponto de partida de uma imagem Docker personalizada. Com isso definido, podemos continuar a adicionar instruções para configurar as aplicações. Essas instruções instalarão as dependências necessárias, copiarão os arquivos da aplicação e configurarão o ambiente de execução.
Adicione o seguinte trecho de código dentro do Dockerfile:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
ADD django-polls/requirements.txt /app/requirements.txt RUN set -ex \ && apk add --no-cache --virtual .build-deps postgresql-dev build-base \ && python -m venv /env \ && /env/bin/pip install --upgrade pip \ && /env/bin/pip install --no-cache-dir -r /app/requirements.txt \ && runDeps="$(scanelf --needed --nobanner --recursive /env \ | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ | sort -u \ | xargs -r apk info --installed \ | sort -u)" \ && apk add --virtual rundeps $runDeps \ && apk del .build-deps ADD django-polls /app WORKDIR /app ENV VIRTUAL_ENV /env ENV PATH /env/bin:$PATH EXPOSE 8000 |
Neste trecho de código, dizemos ao Docker para copiar o requirements.txt para /app/requirements.txt para garantir que as dependências da aplicação estejam disponíveis no sistema de arquivos da imagem. Os requisitos incluem todos os pacotes Python necessários para executar a aplicação. As dependências são copiadas primeiro para que o Docker possa armazenar em cache a camada da imagem. Isso ocorre porque o Docker armazena em cache cada etapa do Dockerfile. A primeira compilação da imagem geralmente é mais demorada. O Docker baixará as dependências e, em seguida, as armazenará em cache. Se o requirements.txt não for alterado, o Docker fará a compilação a partir do cache, tornando as compilações subsequentes mais rápidas.
A próxima etapa possui a instrução RUN que executa uma lista de comandos Linux, encadeados com o operador Linux && . Os comandos fazem o seguinte:
-
Use a ferramenta de gerenciamento de pacotes apk do Alpine para instalar os arquivos de desenvolvimento do PostgreSQL e as dependências básicas de compilação.
-
Crie um ambiente virtual Python.
-
Instale as dependências Python conforme definido no arquivo requirements.txt usando o pip.
-
Compile os pacotes de tempo de execução necessários analisando os requisitos dos pacotes Python instalados.
-
Remova quaisquer dependências de compilação que não sejam mais necessárias.
A razão por trás do encadeamento dos comandos na etapa RUN é reduzir as camadas da imagem. O Docker cria uma nova camada de imagem no topo do sistema de arquivos existente toda vez que encontra ADD, COPY, ou RUN instrução no Dockerfile. Compactar comandos onde aplicável minimizará o número de camadas de imagem criadas.
Itens adicionados às camadas de imagem não podem ser removidos em uma camada subsequente. Você deve declarar instruções para excluir itens indesejados antes de passar para a próxima instrução. Isso é necessário para reduzir o tamanho da imagem. Você deve notar que adicionamos o comando apk del no final da instrução RUN. Isso foi feito para remover as dependências de compilação após as termos usado para compilar os pacotes do aplicativo.
Em seguida, temos outra ADD instrução que usamos para copiar o código do aplicativo para o diretório /app. Em seguida, usaremos a instrução WORKDIR para definir o diretório de trabalho da imagem para o diretório /app que agora contém o código do aplicativo.
Em seguida, temos as instruções ENV que usamos para definir duas variáveis de ambiente que a imagem disponibilizará para os contêineres em execução. Primeiro, definimos a variável VIRTUAL_ENV para /env. Segundo, definimos a variável PATH para incluir o diretório /env/bin. Nessas duas linhas, estamos carregando o script /env/bin/activate, que é como ativamos um ambiente virtual em um ambiente Linux. Você pode ler mais sobre como trabalhar com ambientes virtuais em Python em outros sistemas operacionais. A última instrução é o comando EXPOSE que define a porta 8000 na qual o contêiner escutará em tempo de execução.
A esta altura, seu Dockerfile está quase completo, exceto pelo comando padrão que será executado quando você iniciar os contêineres. Vamos defini-lo na próxima seção.
-
Entendendo o Comando Padrão da Imagem Docker
Ao iniciar um contêiner Docker, você pode fornecer um comando para executar. No entanto, se você não fornecer um comando, o comando padrão da imagem Docker determinará o que acontecerá quando o contêiner for iniciado. Usamos as instruções ENTRYPOINT ou CMD individualmente ou juntas para definir um comando padrão dentro do Dockerfile.
Se você optar por definir tanto ENTRYPOINT quanto CMD, na instrução ENTRYPOINT, você define o executável que será executado pelo contêiner. Na instrução CMD, defina a lista de argumentos padrão para o comando executável. Você pode substituir a lista de argumentos padrão anexando argumentos alternativos na linha de comando ao iniciar o contêiner no formato:
|
1 |
docker run <imagem> <argumentos> |
Este formato impede que os desenvolvedores substituam facilmente o comando ENTRYPOINT. O comando ENTRYPOINT é definido para chamar um script que configurará o ambiente e executará diferentes ações com base na lista de argumentos fornecida.
Você pode usar a instrução ENTRYPOINT sozinha para configurar o executável do contêiner. No entanto, este formato não permite definir uma lista de argumentos padrão. Você pode fornecer argumentos ao executar o contêiner com o comando docker run.
Se você optar por usar apenas CMD , o Docker o interpreta como o comando e a lista de argumentos padrão, que você pode substituir em tempo de execução. Você pode encontrar mais informações nos documentos de referência oficiais do Dockerfile.
Vejamos como podemos aplicar as informações que você aprendeu sobre comandos padrão ao nosso exemplo de contêiner. Queremos servir o aplicativo por padrão usando o servidor gunicorn. Embora a lista de argumentos passada para o servidor gunicorn não precise ser configurável em tempo de execução, queremos a flexibilidade de executar outros comandos para fins como depuração ou gerenciamento de configurações (inicializar o banco de dados, coletar ativos estáticos, etc). Como você pode ver, é do nosso interesse usar o CMD para definir um comando padrão que nos permitirá substituí-lo sempre que necessário.
Aqui estão algumas sintaxes que você pode usar para definir o comando CMD:
- CMD ["comando", "argumento 1", "argumento 2", . . . ,"argumento n"]: O formato exec (formato recomendado) recebe um comando e uma lista de argumentos. Ele executa o comando diretamente sem processamento de shell.
- CMD comando "argumento 1" "argumento 2" . . . "argumento n": O formato shell define um comando e uma lista de argumentos. Ele passa a lista de comandos para o shell para processamento. Você pode achar isso útil se quiser substituir variáveis de ambiente em um comando, no entanto, não é totalmente previsível.
- CMD ["argumento 1", "argumento 2", . . . ,"argumento n"]: O formato de lista de argumentos, ele apenas define la lista de argumentos padrão e é usado em conjunto com uma instrução ENTRYPOINT .
Usaremos o formato exec para definir nossa instrução final no Dockerfile. Adicione a seguinte linha no final do seu Dockerfile:
|
1 |
CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "mysite.wsgi:application"] |
Agora você pode salvar e fechar o Dockerfile.
Quando você iniciar contêineres usando esta imagem, eles executarão gunicorn vinculado à porta do localhost 8000 com 3 workers, e chamarão a application função no arquivo wsgi.py arquivo encontrado no diretório mysite . Você pode optar por fornecer um comando diferente para substituir o comando padrão em tempo de execução e executar um processo diferente em vez do gunicorn. Você pode querer saber mais sobre workers do Gunicorn.
Seu Dockerfile está pronto e você pode usar docker build para compilar a imagem do aplicativo. Você pode usar docker run para iniciar o contêiner em sua máquina de desenvolvimento local.
-
Construindo a imagem Docker
O docker build comando procurará por um Dockerfile no diretório atual por padrão para encontrar suas instruções de compilação. Ele também envia o “contexto” de compilação para o daemon do Docker. Um contexto de compilação é um conjunto de arquivos que devem estar disponíveis durante o processo de compilação. Por padrão, o diretório atual no qual você está executando o comando docker build está definido como o contexto de compilação.
Estando no mesmo diretório que contém seu Dockerfile, execute o comando docker build. Forneça uma imagem e uma tag com a flag -t e defina o diretório atual como contexto de compilação usando o ponto ( .) no final do comando:
|
1 |
docker build -t django-polls:v1 . |
Neste comando, nomeamos a imagem como django-polls e a tag v1. Observe o ponto no final do comando, nós o usamos para denotar o diretório atual como o contexto de compilação.
Quando o docker build for concluído, você deverá ver algo semelhante à seguinte saída:

Sua imagem Docker está pronta. Se não tivéssemos deslocado algumas das configurações para variáveis de ambiente externas, você poderia facilmente executar seu contêiner com o comando docker run . No entanto, como não configuramos as variáveis de ambiente externas que definimos no arquivo settings.py, a execução falhará. Vamos finalizar isso na próxima etapa.
Etapa 8: Configurando o ambiente de execução e testando o aplicativo
Estamos nos aproximando do fim deste tutorial. Nesta etapa, configuraremos as variáveis de ambiente no arquivo env . Com as variáveis do arquivo env definidas, podemos criar o esquema do banco de dados, gerar e enviar os arquivos estáticos para o serviço externo de armazenamento de objetos e, finalmente, testar o aplicativo.
O Docker vem com vários métodos que você pode usar para fornecer variáveis de ambiente ao contêiner. No nosso caso, queremos fornecer uma lista de variáveis de ambiente por meio de um arquivo. Portanto, usaremos o método --env-file método.
Usando seu editor preferido, crie um arquivo chamado env no diretório django_project :
|
1 |
nano env |
Cole a seguinte lista de variáveis:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
DJANGO_SECRET_KEY=your_secret_key DEBUG= DJANGO_LOGLEVEL=info DJANGO_ALLOWED_HOSTS=endereço_IP_do_seu_servidor DB_ENGINE=postgresql_psycopg2 DB_DATABASE=polls_db DB_USERNAME=hackins DB_PASSWORD=sua_senha_do_banco_de_dados DB_HOST=seu_host_do_banco_de_dados DB_PORT=sua_porta_do_banco_de_dados STATIC_DEFAULT_FILE_STORAGE=storages.backends.s3boto3.S3Boto3Storage STATIC_MINIO_BUCKET_NAME=test-bucket MINIO_ACCESS_KEY=sua_chave_de_acesso_minio MINIO_SECRET_KEY=sua_chave_secreta_minio MINIO_URL=sua_url_minio:sua_porta_minio |
As variáveis na lista são as que você definiu nas etapas anteriores:
-
DJANGO_SECRET_KEY: Gere um valor único e imprevisível conforme explicado na documentação do Django. Você pode usar este comando para gerar uma string aleatória e defini-la na variável:
|
1 |
python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())' |
-
DEBUG: Definimos este valor como True, mas para uma implantação em produção, lembre-se de defini-lo como False deixando-o em branco.
-
DJANGO_LOGLEVEL: definimos isso como info, sinta-se à vontade para ajustá-lo ao nível desejado.
-
DJANGO_ALLOWED_HOSTS: defina este valor para o endereço IP do servidor Ubuntu que executa seus contêineres Docker. Opcionalmente, defina-o como *, caractere curinga correspondendo a todos os hosts se estiver em modo de desenvolvimento.
-
DB_DATABASE: se você usou um nome de banco de dados diferente, defina-o aqui adequadamente.
-
DB_USERNAME: defina isso para o nome de usuário que você escolheu para o seu banco de dados.
-
DB_PASSWORD: defina isso para a senha que você escolheu para o seu banco de dados.
-
DB_HOST: defina isso para o host que executa a instância do seu banco de dados, conforme configurado na Etapa Um.
-
DB_PORT: defina isso para a porta do seu banco de dados.
-
STATIC_MINIO_BUCKET_NAME: defina isso para o nome do bucket que você criou na sua conta do MinIO Cloud Storage.
Save and close the file when you finish editing.
As configurações de ambiente estão prontas. Precisamos executar o contêiner passando argumentos para substituir o comando CMD padrão e criar o esquema do banco de dados usando os comandos manage.py makemigrations e manage.py migrate.
Aqui está o comando:
|
1 |
docker run --env-file env django-polls:v1 sh -c "python manage.py makemigrations && python manage.py migrate" |
Neste comando, estamos executando a imagem de contêiner django-polls:v1, usando a flag –env-file para passar o arquivo de variáveis de ambiente. Também substituímos o comando CMD padrão por sh -c "python manage.py makemigrations && python manage.py migrate" Quando este comando for executado para iniciar o contêiner, ele criará o esquema do banco de dados conforme definido no código da aplicação.
Se for bem-sucedido, você deverá ver uma saída semelhante à mostrada abaixo:

A saída indica que o esquema do banco de dados foi criado com sucesso.
A próxima etapa é criar um usuário administrador para o aplicativo Django. Vamos iniciar o contêiner e abrir um shell interativo dentro dele com o seguinte comando:
|
1 |
docker run -i -t --env-file env django-polls:v1 sh |
O comando inicia o contêiner com um prompt de shell que você pode usar para interagir com o shell do Python. Vamos criar um usuário:
|
1 |
python manage.py createsuperuser |
Siga as instruções para fornecer um nome de usuário, endereço de e-mail, senha, digite novamente a senha e pressione enter para criar o usuário. Saia do shell e encerre o contêiner pressionando CTRL+D.
Em seguida, precisamos executar o contêiner novamente, substituindo o comando padrão pelo comando Django collectstatic para gerar os arquivos estáticos do aplicativo e enviá-los para o seu serviço de armazenamento em nuvem MinIO:
|
1 |
docker run --env-file env django-polls:v1 sh -c "python manage.py collectstatic --noinput" |
Quando terminar, você deverá ver uma saída semelhante à abaixo, indicando que seu contêiner se conectou com sucesso ao serviço de armazenamento MinIO e enviou os arquivos estáticos:
![]()
O nosso bucket de armazenamento agora está assim, com os diretórios que o Django criou:

Finalmente, agora podemos executar a aplicação com o comando:
|
1 |
docker run --env-file env -p 80:8000 django-polls:v1 |
Aqui está a saída:

Quando você executa o comando acima, ele executa o comando CMD padrão na sua imagem e expõe a porta 8000 conforme definido. Agora, o Ubuntu na porta 80 é mapeado para a 8000 porta do django-polls:v1 container.
Agora podemos testar a aplicação no navegador. Acesse o endereço IP público do seu servidor no navegador: http://your_server_public_ip.
Espere encontrar um Erro 404 Página Não Encontrada, pois de acordo com o Django Tutorial, não definimos uma rota para o / caminho:

Temos a DEBUG variável definida como True, é por isso que vemos esta página de erro com muitas informações cruciais. Vamos remover a definição da DEBUG variável. Primeiro, você precisará parar o container em execução com CTRL+C. Depois, abra o arquivo env :
|
1 |
nano env |
Em seguida, encontre a DEBUG variável e remova sua definição, ou deixe-a em branco. Nós a deixamos em branco porque a getenv função interpreta False como uma string, retornando assim verdadeiro:
|
1 |
DEBUG= |
Salve o arquivo e execute o container novamente com o comando:
|
1 |
docker run --env-file env -p 80:8000 django-polls:v1 |
Se você visitar este http://your_server_public_ip no seu navegador, você deverá ver a página 404 padrão:

Você viu como pode manipular o comportamento de tempo de execução da sua aplicação Django usando variáveis de ambiente, sem modificar o código-fonte.
Navegue até http://your_server_public_ip/polls para ver a página inicial do Polls:

Não temos enquetes, pois acabamos de implantar a aplicação.
Navegue até a interface de administração: http://your_server_public_ip/admin para visualizar a janela de autenticação do administrador:

Forneça as credenciais que você definiu com o comando createsuperuser para fazer login. Agora você deve estar na interface da página administrativa:

Observe que todos os arquivos estáticos são servidos a partir do serviço de armazenamento externo que configuramos. Você pode clicar com o Botão Direito na janela do seu navegador e selecionar View Page Source:

Você pode adicionar algumas perguntas e opções e testar o desempenho geral da aplicação:

Volte para o índice do Polls http://your_server_public_ip/polls e tente votar na pergunta:

Depois de testar e confirmar que tudo está funcionando como esperado, você pode encerrar o container.
Conclusão
Você configurou com sucesso uma aplicação web Django para funcionar bem em um ambiente baseado em containers. Isso envolveu adaptar a aplicação para funcionar com variáveis de ambiente externas, configurar a aplicação para usar um serviço de armazenamento em nuvem para os arquivos estáticos e criar um Dockerfile para a imagem do container. Você pode visualizar as alterações que fizemos para Dockerizar a aplicação na django-polls-docker branch do repositório django-polls no GitHub.
A partir daqui, as possibilidades são limitadas apenas pela sua imaginação. Você pode configurar o proxy reverso Nginx para ficar entre os clientes e o servidor Guinicorn. Você também pode adicionar o Certbot para obter certificados TLS para proteger seu servidor Nginx. Recomendamos adicionar um proxy HTTP para fazer o buffer de clientes lentos e proteger seu servidor Gunicorn de ataques de negação de serviço.
Embora tenhamos definido 3 workers no comando de inicialização do Dockerfile, você pode definir o número de sua preferência, dependendo dos recursos disponíveis no seu servidor. Você pode encontrar mais informações nos official Gunicorn design docs. Se desejar, você pode enviar a imagem Docker que construiu para o Docker Hub e tentar implantá-la em vários ambientes que tenham o Docker instalado. Se quiser saber mais, continue acompanhando o nosso Тutorials blog pois faremos um tutorial de acompanhamento para tornar a aplicação Django segura com Nginx e Let’s Encrypt.
Finalmente, aqui estão mais recursos que ajudarão você a utilizar o Docker:
- Como Hospedar um Repositório de Imagens Docker e Construir Imagens Docker com uma Instância Autogerenciada do GitLab no Ubuntu 20.04
- Trabalhando com Volumes de Dados Docker no Ubuntu 20.04
- Construir e Implantar uma Aplicação Flask com Docker no Ubuntu 20.04
- Como Implantar o WordPress com Containers Docker no Ubuntu 20.04
Boa computação!
Comentários
Nenhum comentário ainda. Seja o primeiro.