One place for hosting & domains

      Encrypt

      Como escalar e proteger um aplicativo Django com o Docker, Nginx e Let’s Encrypt


      Introdução

      Em ambientes baseados em nuvem, existem várias maneiras de dimensionar e proteger um aplicativo Django. Ao escalar horizontalmente e executar várias cópias de seu aplicativo, você pode construir um sistema mais tolerante e altamente disponível, ao mesmo tempo em que também aumenta seu rendimento de modo que as solicitações possam ser processadas simultaneamente. Uma maneira de escalar horizontalmente um aplicativo Django é fornecendo servidores de aplicativos adicionais que executem seu aplicativo Django e seu servidor HTTP WSGI (como o Gunicorn ou o uWSGI). Para encaminhar e distribuir pedidos recebidos neste conjunto de servidores de aplicativos, você pode usar um balanceador de carga e um proxy reverso como o Nginx. O Nginx também é capaz de colocar em cache conteúdo estático e encerrar as conexões via protocolo TLS, usadas para providenciar o HTTPS e conexões seguras ao seu aplicativo.

      Executar seu aplicativo Django e o proxy Nginx dentro dos contêineres Docker garante que esses componentes se comportem da mesma maneira, independentemente do ambiente em que estão implantados. Além disso, os contêineres proporcionam muitos recursos que facilitam o empacotamento e a configuração do seu aplicativo.

      Neste tutorial, você irá escalar horizontalmente um Django e um aplicativo Gunicorn Polls em contêiner fornecendo dois servidores de aplicativos que irão cada um executar uma cópia de um contêiner de aplicativos Django e Gunicorn.

      Você também habilitará o HTTPS fornecendo e configurando um terceiro servidor proxy que irá executar um contêiner de proxy reverso Nginx e um contêiner do cliente Certbot. O Certbot irá fornecer certificados TLS para o Nginx a partir da autoridade de certificação Let’s Encrypt. Isso irá garantir que seu site receba uma alta classificação de segurança do SSL Labs. Este servidor proxy receberá todos os pedidos externos do seu aplicativo e se colocará em frente aos dois servidores upstream do aplicativo Django. Por fim, você irá fortalecer esse sistema distribuído restringindo o acesso externo apenas ao servidor de proxy.

      Pré-requisitos

      Para seguir este tutorial, será necessário:

      • Três servidores Ubuntu 18.04:

        • Dois serão os servidores de aplicativo, usados para executar os aplicativos Django e Gunicorn.
        • Um servidor será um servidor proxy, usado para executar o Nginx e o Certbot.
        • Todos devem possuir um usuário não root com privilégios sudo e um firewall ativo. Para saber como configurar isso, consulte este guia de Configuração inicial do servidor.
      • Docker instalado em todos os três servidores. Como orientação na instalação do Docker, siga os Passos 1 e 2 de Como instalar e usar o Docker no Ubuntu 18.04.

      • Um nome de domínio registrado. Este tutorial utilizará o your_domain.com durante todo o processo. Você pode obter um domínio gratuitamente através do Freenom, ou usar o registrador de domínios de sua escolha.

      • Um registro de DNS de tipo A com o your_domain.com apontando para o endereço IP público do seu servidor proxy. Você pode seguir esta introdução para o DNS da DigitalOcean para obter mais detalhes sobre como adicioná-lo a uma conta da DigitalOcean, caso seja o que estiver usando:

      • Um bucket de armazenamento de objetos S3, como um espaço da DigitalOcean para armazenar os arquivos estáticos do seu projeto Django e um conjunto de chaves de acesso para esse espaço. Para aprender como criar um espaço, consulte a documentação de produto Como criar espaços. Para aprender como criar chaves de acesso para espaços, consulte Compartilhando acesso a espaços com chaves de acesso. Com pequenas alterações, você pode usar qualquer serviço de armazenamento de objetos que o plug-in django-storages suporte.

      • Uma instância de servidor PostgreSQL, banco de dados e usuário para seu aplicativo Django. Com pequenas alterações, você pode usar qualquer banco de dados compatível com o Django.

      Passo 1 — Configurando o primeiro servidor do aplicativo Django

      Para começar, vamos clonar o repositório do aplicativo Django no primeiro servidor de aplicativo. Em seguida, vamos configurar e compilar a imagem do aplicativo Docker e então testar o aplicativo executando o contêiner do Django.

      Nota: se estiver continuando a partir de Como construir um aplicativo Django e Gunicorn com o Docker, você já terá completado o Passo 1 e pode seguir direto ao Passo 2 para configurar o segundo servidor de aplicativo.

      Comece fazendo login no primeiro dos dois servidores do aplicativo Django e use o git para clonar a ramificação polls-docker a partir do repositório GitHub do aplicativo Polls de tutorial do Django. Este repositório contém o código para o aplicativo de amostra Polls da documentação do Django A ramificação polls-docker contém uma versão em Docker do aplicativo Polls. Para aprender como o aplicativo Polls foi modificado para funcionar efetivamente em um ambiente em contêiner, consulte Como construir um aplicativo Django e Gunicorn com o Docker.

      • git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git

      Navegue até o diretório django-polls:

      cd django-polls
      

      Esse diretório contém o código Python do aplicativo Django, um Dockerfile que o Docker usará para compilar a imagem do contêiner, bem como um arquivo env que contém uma lista de variáveis de ambiente a serem passadas para o ambiente de execução do contêiner. Inspecione o Dockerfile usando o cat:

      cat Dockerfile
      

      Output

      FROM python:3.7.4-alpine3.10 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 CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "mysite.wsgi"]

      Esse Dockerfile usa a imagem Docker oficial do Python 3.7.4 como base e instala os requisitos de pacote Python do Django e do Gunicorn, conforme definido no arquivo django-polls/requirements.txt. Em seguida, ele remove alguns arquivos de compilação desnecessários, copia o código do aplicativo na imagem e define o PATH de execução. Por fim, ele declara que a porta 8000 será usada para aceitar conexões de contêiner recebidas e executa gunicorn com 3 trabalhadores, escutando na porta 8000.

      Para aprender mais sobre cada um dos passos nesse Dockerfile, confira o Passo 6 de Como construir um aplicativo Django e Gunicorn com o Docker.

      Agora, crie a imagem usando o docker build:

      Nós demos o nome de polls para a imagem usando o sinalizador -t e passamos o diretório atual como um contexto de compilação, que é o conjunto de arquivos de referência ao compilar a imagem.

      Depois que o Docker compilar e marcar a imagem, liste as imagens disponíveis usando docker images:

      docker images
      

      Você deve ver a imagem polls listada:

      Output

      REPOSITORY TAG IMAGE ID CREATED SIZE polls latest 80ec4f33aae1 2 weeks ago 197MB python 3.7.4-alpine3.10 f309434dea3a 8 months ago 98.7MB

      Antes de executarmos o contêiner Django, precisamos configurar seu ambiente de execução usando o arquivo env presente no diretório atual. Esse arquivo será passado para o comando docker run usado para executar contêiner e o Docker irá injetar as variáveis de ambiente configuradas no ambiente de execução do contêiner.

      Abra o arquivo env com o nano ou com o seu editor favorito:

      nano env
      

      Vamos configurar o arquivo dessa forma, e você precisará adicionar alguns valores adicionais, conforme descrito abaixo.

      django-polls/env

      DJANGO_SECRET_KEY=
      DEBUG=True
      DJANGO_ALLOWED_HOSTS=
      DATABASE_ENGINE=postgresql_psycopg2
      DATABASE_NAME=polls
      DATABASE_USERNAME=
      DATABASE_PASSWORD=
      DATABASE_HOST=
      DATABASE_PORT=
      STATIC_ACCESS_KEY_ID=
      STATIC_SECRET_KEY=
      STATIC_BUCKET_NAME=
      STATIC_ENDPOINT_URL=
      DJANGO_LOGLEVEL=info
      

      Preencha os valores que estão faltando para as seguintes chaves:

      • DJANGO_SECRET_KEY: defina isso como um valor único e imprevisível, conforme detalhado na documentação do Django. Um método para gerar essa chave é fornecido em Ajustando as configurações do aplicativo do tutorial sobre o Aplicativo Django escalável.
      • DJANGO_ALLOWED_HOSTS: essa variável protege o aplicativo e impede ataques de cabeçalho de host HTTP. Para fins de teste, defina isso como *, um coringa que irá corresponder a todos os hosts. Na produção, você deve definir isso como your_domain.com. Para aprender mais sobre esse ajuste do Django, consulte as Core Settings da documentação do Django.
      • DATABASE_USERNAME: defina isso como o usuário do banco de dados PostgreSQL criado nos passos pré-requisitos.
      • DATABASE_NAME: defina isso como polls ou o nome do banco de dados PostgreSQL criado nos passos pré-requisitos.
      • DATABASE_PASSWORD: defina isso como a senha do usuário do banco de dados PostgreSQL criada nos passos pré-requisitos.
      • DATABASE_HOST: defina isso como o nome do host do seu banco de dados.
      • DATABASE_PORT: defina isso como a porta do seu banco de dados.
      • STATIC_ACCESS_KEY_ID: defina isso como a chave de acesso do seu bucket S3 ou espaço.
      • STATIC_SECRET_KEY: defina isso como o segredo da chave de acesso do seu bucket S3 ou espaço.
      • STATIC_BUCKET_NAME: defina isso como o nome do seu bucket S3 ou espaço.
      • STATIC_ENDPOINT_URL: defina isso como o URL do ponto de extremidade do bucket S3 ou espaço apropriado, como por exemplo https://space-name.nyc3.digitaloceanspaces.com se seu espaço estiver localizado na região nyc3.

      Assim que terminar a edição, salve e feche o arquivo.

      Agora, usaremos o docker run para substituir o conjunto CMD no Dockerfile e criar o esquema de banco de dados usando os comandos manage.py makemigrations e manage.py migrate:

      docker run --env-file env polls sh -c "python manage.py makemigrations && python manage.py migrate"
      

      Executamos a imagem de contêiner polls:latest, passamos o arquivo de variável de ambiente que acabamos de modificar e substituímos o comando do Dockerfile com sh -c "python manage.py makemigrations && python manage.py migrate", o que irá criar o esquema de banco de dados definido pelo código do aplicativo. Se estiver executando isso pela primeira vez, você deve ver:

      Output

      No changes detected Operations to perform: Apply all migrations: admin, auth, contenttypes, polls, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying polls.0001_initial... OK Applying sessions.0001_initial... OK

      Isso indica que o esquema de banco de dados foi criado com sucesso.

      Se estiver executando migrate uma outra vez, o Django irá cancelar a operação a menos que o esquema de banco de dados tenha sido alterado.

      Em seguida, vamos executar outra instância do contêiner de aplicativo e usar um shell interativo dentro dela para criar um usuário administrativo para o projeto Django.

      docker run -i -t --env-file env polls sh
      

      Isso lhe fornecerá um prompt do shell dentro do contêiner em execução que você pode usar para criar o usuário do Django:

      python manage.py createsuperuser
      

      Digite um nome de usuário, endereço de e-mail e senha para o seu usuário e, depois de criá-lo, pressione CTRL+D para sair do contêiner e encerrá-lo.

      Por fim, vamos gerar os arquivos estáticos para o aplicativo e fazer o upload deles para o espaço da DigitalOcean usando o collectstatic. Observe que esse processo pode demorar um pouco de tempo para ser concluído.

      docker run --env-file env polls sh -c "python manage.py collectstatic --noinput"
      

      Depois que esses arquivos forem gerados e enviados, você receberá a seguinte saída.

      Output

      121 static files copied.

      Agora, podemos executar o aplicativo:

      docker run --env-file env -p 80:8000 polls
      

      Output

      [2019-10-17 21:23:36 +0000] [1] [INFO] Starting gunicorn 19.9.0 [2019-10-17 21:23:36 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1) [2019-10-17 21:23:36 +0000] [1] [INFO] Using worker: sync [2019-10-17 21:23:36 +0000] [7] [INFO] Booting worker with pid: 7 [2019-10-17 21:23:36 +0000] [8] [INFO] Booting worker with pid: 8 [2019-10-17 21:23:36 +0000] [9] [INFO] Booting worker with pid: 9

      Aqui, executamos o comando padrão definido no Dockerfile, gunicorn --bind :8000 --workers 3 mysite.wsgi:application e expomos a porta do contêiner 8000 para que a porta 80 no servidor Ubuntu seja mapeada para a porta 8000 do contêiner polls.

      Agora, você deve ser capaz de navegar até o aplicativo polls usando seu navegador Web digitando http://APP_SERVER_1_IP na barra de URL. Como não há nenhuma rota definida para o caminho /, você provavelmente receberá um erro 404 Page Not Found, o que é esperado.

      Aviso: quando se usa o firewall UFW com o Docker, o Docker ignora quaisquer regras configuradas do firewall UFW, conforme documentado neste problema do GitHub. Isso explica por que você tem acesso à porta 80 do seu servidor, mesmo que não tenha criado explicitamente uma regra de acesso no UFW em qualquer passo pré-requisito. No Passo 5, vamos tratar desse problema de segurança corrigindo a configuração do UFW. Se você não estiver usando o UFW e estiver usando os Firewalls em Nuvem da DigitalOcean, você pode ignorar com segurança esse aviso.

      Navegue até http://APP_SERVER_1_IP/polls para ver a interface do aplicativo Polls:

      Interface do aplicativo Polls

      Para visualizar a interface administrativa, visite http://APP_SERVER_1_IP/admin. Você deve ver a janela de autenticação do administrador do aplicativo Polls:

      Página de autenticação de administrador do Polls

      Digite o nome e a senha do usuário administrativo que você criou com o comando createsuperuser.

      Depois de autenticar-se, você pode acessar a interface administrativa do aplicativo Polls:

      Interface administrativa principal do Polls

      Observe que os ativos estáticos para os aplicativos admin e polls estão sendo entregues diretamente do armazenamento de objetos. Para confirmar isso, consulte Testando a entrega de arquivos estáticos de espaços.

      Quando terminar de explorar, aperte CTRL+C na janela do terminal executando o contêiner Docker para encerrar o contêiner.

      Agora que confirmou que o contêiner de aplicativo funciona como esperado, você pode executá-lo em modo separado. Isso irá executá-lo em segundo plano e lhe permitirá fazer logoff da sua sessão SSH:

      docker run -d --rm --name polls --env-file env -p 80:8000 polls
      

      O sinalizador -d instrui o Docker a executar o contêiner em modo separado, o sinalizador -rm limpa o sistema de arquivos do contêiner após a saída do contêiner e damos o nome de polls a ele.

      Faça logoff do primeiro servidor do aplicativo Django e navegue até http://APP_SERVER_1_IP/polls para confirmar se o contêiner está funcionando como esperado.

      Agora que seu primeiro servidor do aplicativo Django está em operação, você pode configurar seu segundo servidor do aplicativo Django.

      Passo 2 — Configurando o segundo servidor do aplicativo Django

      Como muitos dos comandos usados para configurar este servidor serão iguais àqueles do passo anterior, eles serão apresentados aqui de forma abreviada. Por favor, reveja o Passo 1 para obter mais informações sobre qualquer comando em particular neste passo.

      Comece fazendo login no segundo servidor do aplicativo Django.

      Clone a ramificação polls-docker do repositório GitHub django-polls:

      • git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git

      Navegue até o diretório django-polls:

      cd django-polls
      

      Compile a imagem usando o docker build:

      Abra o arquivo env com o nano ou com o seu editor favorito:

      nano env
      

      django-polls/env

      DJANGO_SECRET_KEY=
      DEBUG=True
      DJANGO_ALLOWED_HOSTS=
      DATABASE_ENGINE=postgresql_psycopg2
      DATABASE_NAME=polls
      DATABASE_USERNAME=
      DATABASE_PASSWORD=
      DATABASE_HOST=
      DATABASE_PORT=
      STATIC_ACCESS_KEY_ID=
      STATIC_SECRET_KEY=
      STATIC_BUCKET_NAME=
      STATIC_ENDPOINT_URL=
      DJANGO_LOGLEVEL=info
      

      Preencha os valores que estão faltando como no Passo 1. Quando terminar a edição, salve e feche o arquivo.

      Por fim, execute o contêiner do aplicativo em modo separado:

      docker run -d --rm --name polls --env-file env -p 80:8000 polls
      

      Navegue até http://APP_SERVER_2_IP/polls para confirmar se o contêiner está funcionando como esperado. Você pode fazer logoff com segurança do segundo servidor de aplicativo sem precisar encerrar seu contêiner em execução.

      Com ambos os contêineres do aplicativo Django em operação, você pode seguir para a configuração do contêiner do proxy reverso do Nginx.

      Passo 3 — Configurando o contêiner Docker do Nginx

      O Nginx é um servidor Web versátil que oferece vários recursos, incluindo proxy reverso, balanceamento de carga e cache. Neste tutorial, nós descarregamos os ativos estáticos do Django para um armazenamento de objetos, então não usaremos as capacidades de cache do Nginx. No entanto, usaremos o Nginx como um proxy reverso para nossos dois servidores de backend do aplicativo Django e distribuiremos pedidos recebidos entre eles. Além disso, o Nginx irá realizar a terminação TLS e redirecionamento utilizando um certificado TLS fornecido pelo Certbot. Isso significa que ele irá forçar os clientes a usar o HTTPS, redirecionando solicitações HTTP recebidas para a porta 443. Em seguida, irá descriptografar as solicitações HTTPS e enviá-las via proxy para os servidores upstream do Django.

      Neste tutorial, tomamos a decisão de projeto de dissociar os contêineres do Nginx dos servidores de backend. Dependendo do seu caso de uso, você pode optar por executar o contêiner do Nginx em um dos servidores do aplicativo Django, fazendo o proxy de pedidos localmente, bem como para o outro servidor Django. Outra arquitetura possível seria executar dois contêineres do Nginx, um em cada servidor de backend, com um balanceador de carga em nuvem no frontend. Cada arquitetura apresenta diferentes vantagens de segurança e desempenho, e você deve fazer o teste de carga no seu sistema para descobrir gargalos. A arquitetura flexível descrita neste tutorial permite que você escale tanto a camada backend do aplicativo Django, quanto a camada de proxy do Nginx. Assim que o contêiner do Nginx único se tornar um gargalo, você pode escalar o processo para vários proxies do Nginx e adicionar um balanceador de carga em nuvem ou um balanceador de carga L4 rápido como o HAProxy.

      Com ambos os servidores do aplicativo Django em operação, podemos começar a configurar o servidor de proxy do Nginx. Faça login em seu servidor de proxy e crie um diretório chamado conf:

      mkdir conf
      

      Crie um arquivo de configuração chamado nginx.conf usando o nano ou seu editor favorito:

      nano conf/nginx.conf
      

      Cole nele a seguinte configuração do Nginx:

      conf/nginx.conf

      
      upstream django {
          server APP_SERVER_1_IP;
          server APP_SERVER_2_IP;
      }
      
      server {
          listen 80 default_server;
          return 444;
      }
      
      server {
          listen 80;
          listen [::]:80;
          server_name your_domain.com;
          return 301 https://$server_name$request_uri;
      }
      
      server {
          listen 443 ssl http2;
          listen [::]:443 ssl http2;
          server_name your_domain.com;
      
          # SSL
          ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
          ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
      
          ssl_session_cache shared:le_nginx_SSL:10m;
          ssl_session_timeout 1440m;
          ssl_session_tickets off;
      
          ssl_protocols TLSv1.2 TLSv1.3;
          ssl_prefer_server_ciphers off;
      
          ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
      
          client_max_body_size 4G;
          keepalive_timeout 5;
      
              location / {
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
                proxy_set_header Host $http_host;
                proxy_redirect off;
                proxy_pass http://django;
              }
      
          location ^~ /.well-known/acme-challenge/ {
              root /var/www/html;
          }
      
      }
      

      Esses blocos upstream, server e location configuram o Nginx para redirecionar solicitações HTTP para HTTPS, além do balanceamento de carga entre eles em dois servidores do aplicativo Django configurados nos Passos 1 e 2. Para aprender mais sobre a estrutura do arquivo de configuração do Nginx, consulte este artigo, Compreendendo a estrutura do arquivo de configuração do Nginx e contextos de configuração. Além dele, este artigo, Compreendendo os algoritmos de seleção do servidor Nginx e do bloco de localização, também pode ser útil.

      Essa configuração foi montada a partir de arquivos de configuração de amostra fornecidos pelo Gunicorn, Cerbot e Nginx e serve como uma configuração mínima do Nginx para tornar essa arquitetura funcional. Ajustar essa configuração do Nginx vai além do escopo deste artigo, mas você pode usar uma ferramenta como o NGINXConfig para gerar arquivos de configuração do Nginx com maior desempenho e segurança para sua arquitetura.

      O bloco upstream define o grupo de servidores usados para fazer proxy de solicitações usando a diretiva proxy_pass:

      conf/nginx.conf

      upstream django {
          server APP_SERVER_1_IP;
          server APP_SERVER_2_IP;
      }
      . . .
      

      Neste bloco, damos o nome de django ao upstream e incluímos os endereços IP de ambos os servidores do aplicativo Django. Se os servidores do aplicativo estiverem em execução na DigitalOcean e possuírem a rede VPC ativada, você deve usar aqui seus endereços IP privados. Para aprender como habilitar a rede VPC na DigitalOcean, consulte Como habilitar a rede VPC em Droplets existentes.

      O primeiro bloco server captura solicitações que não correspondam ao seu domínio e encerra a conexão. Por exemplo, uma solicitação HTTP direta para o endereço IP do seu servidor seria manipulada por este bloco:

      conf/nginx.conf

      . . .
      server {
          listen 80 default_server;
          return 444;
      }
      . . .
      

      O próximo bloco server redireciona as solicitações HTTP para seu domínio para HTTPS usando um redirecionamento HTTP 301. Essas solicitações são então manipuladas pelo bloco server final:

      conf/nginx.conf

      . . .
      server {
          listen 80;
          listen [::]:80;
          server_name your_domain.com;
          return 301 https://$server_name$request_uri;
      }
      . . .
      

      Essas duas diretivas definem os caminhos para o certificado TLS e a chave secreta. Eles serão fornecidos usando o Certbot e montados no contêiner do Nginx no próximo passo.

      conf/nginx.conf

      . . .
      ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
      ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
      . . .
      

      Esses parâmetros são os padrões de segurança SSL recomendados pelo Certbot. Para aprender mais sobre eles, consulte Module ngx_http_ssl_module da documentação do Nginx. O Security/Server Side TLS do Mozilla é outro guia útil que você ajuste sua configuração SSL.

      conf/nginx.conf

      . . .
          ssl_session_cache shared:le_nginx_SSL:10m;
          ssl_session_timeout 1440m;
          ssl_session_tickets off;
      
          ssl_protocols TLSv1.2 TLSv1.3;
          ssl_prefer_server_ciphers off;
      
          ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
      . . .
      

      Essas duas diretivas da amostra de configuração do Nginx do Gunicorn definem o tamanho máximo permitido do corpo da solicitação do cliente e atribuem o tempo limite para manutenção de conexão com o cliente. O Nginx irá interromper as conexões com o cliente após keepalive_timeout segundos.

      conf/nginx.conf

      . . .
      client_max_body_size 4G;
      keepalive_timeout 5;
      . . .
      

      O primeiro bloco location instrui o Nginx a redirecionar solicitações via proxy para os servidores upstream django via HTTP. Ele também preserva os cabeçalhos HTTP do cliente que capturam o endereço IP originário, protocolo usado para conexão e host de destino:

      conf/nginx.conf

      . . .
      location / {
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header X-Forwarded-Proto $scheme;
          proxy_set_header Host $http_host;
          proxy_redirect off;
          proxy_pass http://django;
      }
      . . .
      

      Para aprender mais sobre essas diretivas, consulte Deploying Gunicorn e Module ngx_http_proxy_module da documentação do Nginx.

      O bloco location final captura as solicitações para o caminho /well-known/acme-challenge/ usado pelo Certbot para HTTP-01 challenges. Ele faz isso para verificar seu domínio com o Let’s Encrypt e fornecer ou renovar os certificados TLS. Para maiores informações sobre o HTTP-01 challenge usado pelo Certbot, consulte Challenge Types da documentação do Let’s Encrypt.

      conf/nginx.conf

      . . .
      location ^~ /.well-known/acme-challenge/ {
              root /var/www/html;
      }
      

      Assim que terminar a edição, salve e feche o arquivo.

      Agora, você pode usar esse arquivo de configuração para executar um contêiner Docker do Nginx. Neste tutorial, usaremos a imagem nginx:1.19.0, versão 1.19.0 da imagem oficial do Docker mantida pelo Nginx.

      Quando executamos o contêiner pela primeira vez, o Nginx irá gerar um erro e falhar, pois ainda não fornecemos os certificados definidos no arquivo de configuração. No entanto, ainda vamos executar o comando para baixar localmente a imagem do Nginx e testar se todo o resto está funcionando corretamente:

      docker run --rm --name nginx -p 80:80 -p 443:443 
          -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro 
          -v /var/www/html:/var/www/html 
          nginx:1.19.0
      

      Aqui, chamamos o contêiner de nginx e mapeamos as portas de host 80 e 443 para as respectivas portas do contêiner. O sinalizador -v monta o arquivo de configuração no contêiner do Nginx em /etc/nginx/conf.d/nginx.conf, que é de onde a imagem do Nginx está configurada para carregar. Ele é montado em ro, ou modo “apenas leitura”, para que o contêiner não consiga modificar o arquivo. O diretório root Web /var/www/html também é montado no contêiner. Por fim, o nginx:1.19.0 instrui o Docker a pegar e executar a imagem nginx:1.19.0 do Dockerhub.

      O Docker irá pegar e executar a imagem, e então o Nginx irá gerar um erro quando não encontrar o certificado TLS e a chave secreta configurados. No próximo passo, vamos providenciá-los usando um cliente Certbot em Docker e a autoridade de certificação Let’s Encrypt.

      Passo 4 — Configurando o Certbot e a renovação de certificados do Lets Encrypt

      O Certbot é um cliente do Let’s Encrypt desenvolvido pela Electronic Frontier Foundation. Ele fornece certificados TLS gratuitos da autoridade de certificação Let’s Encrypt, que permite que navegadores verifiquem a identidade de seus servidores Web. Como temos o Docker instalado em nosso servidor de proxy do Nginx, usaremos a imagem Certbot do Docker para fornecer e renovar os certificados TLS.

      Comece garantindo que você tenha um registro DNS do tipo A mapeado para o endereço IP público do servidor proxy. Em seguida, em seu servidor proxy, forneça uma versão de preparo dos certificados usando a imagem certbot do Docker:

      docker run -it --rm -p 80:80 --name certbot 
               -v "/etc/letsencrypt:/etc/letsencrypt" 
               -v "/var/lib/letsencrypt:/var/lib/letsencrypt" 
               certbot/certbot certonly --standalone --staging -d your_domain.com
      

      Esse comando executa a imagem certbot do Docker em modo interativo, e encaminha a porta 80 no host para a porta 80 no contêiner. Ele cria e monta dois diretórios de host no contêiner: /etc/letsencrypt/ e /var/lib/letsencrypt/. O certbot é executado no modo standalone, sem o Nginx, e usará os servidores staging — de preparo — do Let’s Encrypt para realizar a validação de domínio.

      Quando solicitado, digite seu endereço de e-mail e concorde com os Termos de serviço. Se a validação de domínio for bem sucedida, você deve ver o seguinte resultado:

      Output

      Obtaining a new certificate Performing the following challenges: http-01 challenge for stubb.dev Waiting for verification... Cleaning up challenges IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/your_domain.com/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/your_domain.com/privkey.pem Your cert will expire on 2020-09-15. To obtain a new or tweaked version of this certificate in the future, simply run certbot again. To non-interactively renew *all* of your certificates, run "certbot renew" - Your account credentials have been saved in your Certbot configuration directory at /etc/letsencrypt. You should make a secure backup of this folder now. This configuration directory will also contain certificates and private keys obtained by Certbot so making regular backups of this folder is ideal.

      Você pode inspecionar o certificado usando o cat:

      sudo cat /etc/letsencrypt/live/your_domain.com/fullchain.pem
      

      Com o certificado TLS fornecido, podemos testar a configuração do Nginx montada no passo anterior:

      docker run --rm --name nginx -p 80:80 -p 443:443 
          -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro 
          -v /etc/letsencrypt:/etc/letsencrypt 
          -v /var/lib/letsencrypt:/var/lib/letsencrypt 
          -v /var/www/html:/var/www/html 
          nginx:1.19.0
      

      Esse é a mesma execução de comandos do Passo 3, com a adição de ambos os diretórios recém-criados do Let’s Encrypt.

      Assim que o Nginx estiver em funcionamento, navege até http://your_domain.com. Você pode receber um aviso em seu navegador de que a autoridade de certificação é inválida. Isso é esperado, já que fornecemos os certificados de preparo e não os certificados do Let’s Encrypt de produção. Verifique a barra de URL do seu navegador para confirmar se sua solicitação HTTP foi redirecionada para o HTTPS.

      Aperte CTRL+C em seu terminal para sair do Nginx e execute o cliente certbot novamente, mas, desta vez, omitindo o sinalizador --staging:

      docker run -it --rm -p 80:80 --name certbot 
               -v "/etc/letsencrypt:/etc/letsencrypt" 
               -v "/var/lib/letsencrypt:/var/lib/letsencrypt" 
               certbot/certbot certonly --standalone -d your_domain.com
      

      Quando solicitado a manter o certificado existente ou renová-lo, aperte 2 para renová-lo e então ENTER para confirmar sua escolha.

      Com o certificado TLS de produção fornecido, execute o servidor Nginx novamente:

      docker run --rm --name nginx -p 80:80 -p 443:443 
          -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro 
          -v /etc/letsencrypt:/etc/letsencrypt 
          -v /var/lib/letsencrypt:/var/lib/letsencrypt 
          -v /var/www/html:/var/www/html 
          nginx:1.19.0
      

      Em seu navegador, navegue até http://your_domain.com. Na barra de URL, confirme se a solicitação HTTP foi redirecionada para o HTTPS. Como o aplicativo Polls não possui nenhum padrão de rota configurado, você deve ver um erro Page not found do Django. Navegue até https://your_domain.com/polls e você verá a interface padrão do aplicativo Polls:

      Interface do aplicativo Polls

      Neste ponto, você já forneceu um certificado TLS de produção usando o cliente Certbot do Docker, e está aplicando um proxy reverso e balanceamento de carga nas solicitações externas de carga para os dois servidores do aplicativo Django.

      Os certificados do Let’s Encrypt expiram a cada 90 dias. Para garantir que seu certificado permaneça válido, você deve renová-lo regularmente antes de sua expiração programada. Com o Nginx em execução, você deve usar o cliente Certbot no modo webroot em vez do modo standalone. Isso significa que o Certbot irá realizar a validação criando um arquivo no diretório /var/www/html/.well-known/acme-challenge/ e as solicitações de validação do Let’s Encrypt para este caminho serão capturadas pela regra location definida na configuração do Nginx no Passo 3. Então, o Certbot irá rotacionar os certificados e você pode recarregar o Nginx para que ele use esse certificado recém-fornecido.

      Existem várias maneiras de automatizar esse procedimento e a renovação automática de certificados TLS vai além do escopo deste tutorial. Para um processo semelhante usando o utilitário de planejamento cron, consulte o Passo 6 de Como proteger um aplicativo Node.js em contêiner com Nginx, Let’s Encrypt e Docker Compose.

      Em seu terminal, aperte CTRL+C para encerrar o contêiner do Nginx. Execute-o novamente em modo separado adicionando o sinalizador -d:

      docker run --rm --name nginx -d -p 80:80 -p 443:443 
          -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro 
          -v /etc/letsencrypt:/etc/letsencrypt 
          -v /var/lib/letsencrypt:/var/lib/letsencrypt 
        -v /var/www/html:/var/www/html 
          nginx:1.19.0
      

      Com o Nginx em execução em segundo plano, use o comando a seguir para realizar simulação do procedimento de renovação de certificado:

      docker run -it --rm --name certbot 
          -v "/etc/letsencrypt:/etc/letsencrypt" 
        -v "/var/lib/letsencrypt:/var/lib/letsencrypt" 
        -v "/var/www/html:/var/www/html" 
        certbot/certbot renew --webroot -w /var/www/html --dry-run
      

      Usamos o plug-in --webroot, especificamos o caminho do root Web e usamos o sinalizador --dry-run para verificar se tudo está funcionando corretamente sem realmente realizar a renovação de certificado.

      Se a simulação de renovação for bem sucedida, você deve ver o seguinte resultado:

      Output

      Cert not due for renewal, but simulating renewal for dry run Plugins selected: Authenticator webroot, Installer None Renewing an existing certificate Performing the following challenges: http-01 challenge for your_domain.com Using the webroot path /var/www/html for all unmatched domains. Waiting for verification... Cleaning up challenges - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - new certificate deployed without reload, fullchain is /etc/letsencrypt/live/your_domain.com/fullchain.pem - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates below have not been saved.) Congratulations, all renewals succeeded. The following certs have been renewed: /etc/letsencrypt/live/your_domain.com/fullchain.pem (success) ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates above have not been saved.) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      Em uma configuração de produção, após renovar os certificados, é necessário recarregar o Nginx para que as alterações entrem em vigor. Para recarregar o Nginx, execute o seguinte comando:

      docker kill -s HUP nginx
      

      Esse comando enviará um sinal HUP do Unix para o processo do Nginx em execução dentro do contêiner nginx do Docker. Ao receber esse sinal, o Nginx irá recarregar suas configurações e os certificados renovados.

      Com o HTTPS habilitado e todos os componentes dessa arquitetura em operação, o passo final é bloquear a configuração impedindo o acesso externo aos dois servidores backend de aplicativo. Todas as solicitações HTTP devem fluir através do proxy do Nginx.

      Passo 5 — Prevenindo o acesso externo a servidores do aplicativo Django

      Na arquitetura descrita neste tutorial, a terminação SSL ocorre no proxy do Nginx. Isso significa que o Nginx descriptografa a conexão SSL, e os pacotes, não criptografados, são enviados via proxy para os servidores do aplicativo Django. Para muitos casos de uso, esse nível de segurança é o suficiente. Para aplicações envolvendo dados financeiros ou de saúde, pode ser interessante implementar uma criptografia de ponta a ponta. Você pode fazer isso encaminhando pacotes criptografados através do balanceador de carga e descriptografando-os nos servidores do aplicativo, ou criptografando novamente no proxy e mais uma vez descriptografando nos servidores do aplicativo Django. Essas técnicas vão além do escopo deste artigo, mas para aprender mais sobre elas, consulte Criptografia de ponta a ponta.

      O proxy do Nginx age como um gateway entre o tráfego externo e a rede interna. Teoricamente, nenhum cliente externo deve ter acesso direto aos servidores internos do aplicativo, e todas as solicitações devem fluir através do servidor Nginx. A nota no Passo 1 descreve brevemente um problema em aberto com o Docker, onde ele ignora as configurações de firewall do ufw por padrão e abre portas externamente, o que pode não ser seguro. Para lidar com essa preocupação de segurança, é recomendado usar firewalls em nuvem ao trabalhar com servidores que possuam o Docker habilitado. Para obter mais informações sobre a criação de firewalls em nuvem com a DigitalOcean, consulte Como criar firewalls. Você também pode manipular o iptables diretamente em vez de usar o ufw. Para aprender mais sobre como usar o iptables com o Docker, consulte Docker e o iptables.

      Neste passo, vamos modificar a configuração do UFW para bloquear o acesso externo a portas do host abertas pelo Docker. Ao executarmos o Django nos servidores do aplicativo, passamos o sinalizador -p 80:8000 para o docker, que encaminha a porta 80 no host para a porta 8000 no contêiner. Isso também abriu a porta 80 para clientes externos, o que pode ser verificado visitando http://your_app_server_1_IP. Para evitar o acesso direto, vamos modificar a configuração do UFW usando o método descrito no repositório ufw-docker do GitHub.

      Comece fazendo login no primeiro servidor do aplicativo Django. Em seguida, abra o arquivo /etc/ufw/after.rules com privilégios de superusuário, usando o nano ou o seu editor favorito:

      sudo nano /etc/ufw/after.rules
      

      Digite sua senha quando solicitado, e aperte ENTER para confirmar.

      Você deve ver as seguintes regras do ufw:

      /etc/ufw/after.rules

      #
      # rules.input-after
      #
      # Rules that should be run after the ufw command line added rules. Custom
      # rules should be added to one of these chains:
      #   ufw-after-input
      #   ufw-after-output
      #   ufw-after-forward
      #
      
      # Don't delete these required lines, otherwise there will be errors
      *filter
      :ufw-after-input - [0:0]
      :ufw-after-output - [0:0]
      :ufw-after-forward - [0:0]
      # End required lines
      
      # don't log noisy services by default
      -A ufw-after-input -p udp --dport 137 -j ufw-skip-to-policy-input
      -A ufw-after-input -p udp --dport 138 -j ufw-skip-to-policy-input
      -A ufw-after-input -p tcp --dport 139 -j ufw-skip-to-policy-input
      -A ufw-after-input -p tcp --dport 445 -j ufw-skip-to-policy-input
      -A ufw-after-input -p udp --dport 67 -j ufw-skip-to-policy-input
      -A ufw-after-input -p udp --dport 68 -j ufw-skip-to-policy-input
      
      # don't log noisy broadcast
      -A ufw-after-input -m addrtype --dst-type BROADCAST -j ufw-skip-to-policy-input
      
      # don't delete the 'COMMIT' line or these rules won't be processed
      COMMIT
      

      Role até o final e cole o bloco a seguir de regras de configuração do UFW:

      /etc/ufw/after.rules

      . . .
      
      # BEGIN UFW AND DOCKER
      *filter
      :ufw-user-forward - [0:0]
      :DOCKER-USER - [0:0]
      -A DOCKER-USER -j RETURN -s 10.0.0.0/8
      -A DOCKER-USER -j RETURN -s 172.16.0.0/12
      -A DOCKER-USER -j RETURN -s 192.168.0.0/16
      
      -A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN
      
      -A DOCKER-USER -j ufw-user-forward
      
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12
      
      -A DOCKER-USER -j RETURN
      COMMIT
      # END UFW AND DOCKER
      

      Essas regras restringem o acesso público às portas abertas pelo Docker e permitem o acesso dos intervalos de IP privativo 10.0.0.0/8, 172.16.0.0/12 e 192.168.0.0/16. Se estiver usando o VPC com a DigitalOcean, então os Droplets em sua rede VPC terão acesso à porta aberta através da interface de rede privada, mas os clientes externos não terão. Para maiores informações sobre o VPC, consulte a documentação oficial do VPC. Para aprender mais sobre as regras implementadas nesse trecho de código, consulte Como isso funciona? do LEIAME do ufw-docker.

      Se você não estiver usando o VPC com a DigitalOcean, e digitou os endereços IP públicos dos servidores do aplicativo no bloco upstream de sua configuração do Nginx, será necessário modificar explicitamente o firewall UFW para permitir o tráfego do servidor Nginx através da porta 80 nos servidores do aplicativo Django. Para orientação sobre a criação de regras de allow (permissão) com o firewall UFW, consulte Fundamentos do UFW: Regras e comandos comuns do firewall.

      Assim que terminar a edição, salve e feche o arquivo.

      Reinicie o ufw para que a nova configuração entre em vigor:

      sudo systemctl restart ufw
      

      Navegue até http://APP_SERVER_1_IP em seu navegador Web para confirmar se não é mais possível acessar o servidor do aplicativo pela porta 80.

      Repita esse processo no segundo servidor do aplicativo Django.

      Faça logoff do primeiro servidor de aplicativo ou abra outra janela do terminal e faça login no segundo servidor do aplicativo Django. Em seguida, abra o arquivo /etc/ufw/after.rules com privilégios de superusuário, usando o nano ou o seu editor favorito:

      sudo nano /etc/ufw/after.rules
      

      Digite sua senha quando solicitado e aperte ENTER para confirmar.

      Role até o final e cole o bloco a seguir de regras de configuração do UFW:

      /etc/ufw/after.rules

      . . .
      
      # BEGIN UFW AND DOCKER
      *filter
      :ufw-user-forward - [0:0]
      :DOCKER-USER - [0:0]
      -A DOCKER-USER -j RETURN -s 10.0.0.0/8
      -A DOCKER-USER -j RETURN -s 172.16.0.0/12
      -A DOCKER-USER -j RETURN -s 192.168.0.0/16
      
      -A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN
      
      -A DOCKER-USER -j ufw-user-forward
      
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12
      
      -A DOCKER-USER -j RETURN
      COMMIT
      # END UFW AND DOCKER
      

      Assim que terminar a edição, salve e feche o arquivo.

      Reinicie o ufw para que a nova configuração entre em vigor:

      sudo systemctl restart ufw
      

      Navegue até http://APP_SERVER_2_IP em seu navegador Web para confirmar se não é mais possível acessar o servidor do aplicativo pela porta 80.

      Por fim, navegue até https://your_domain_here/polls para confirmar se o proxy do Nginx ainda tem acesso aos servidores upstream do Django. Você deve ver a interface padrão do aplicativo Polls.

      Conclusão

      Neste tutorial, você configurou um aplicativo Polls do Django escalável usando contêineres Docker. À medida que seu tráfego cresce e a carga no sistema aumenta, é possível escalar cada camada separadamente: a camada de proxy do Nginx, a camada backend do aplicativo Django e a camada do banco de dados PostgreSQL.

      Ao construir um sistema distribuído, muitas vezes há várias decisões de projeto que você deve enfrentar, e várias arquiteturas podem satisfazer seu caso de uso. A arquitetura descrita neste tutorial tem o intuito de servir como um planejamento flexível para projetar aplicativos escaláveis com o Django e o Docker.

      Você pode desejar controlar o comportamento dos contêineres quando eles encontram erros, ou executar contêineres automaticamente quando o seu sistema for inicializado. Para fazer isso, você pode usar um gerenciador de processos como o Systemd ou implementar políticas de reinicialização. Para mais informações sobre isso, consulte Iniciar contêineres automaticamente da documentação do Docker.

      Ao trabalhar em escala com vários hosts executando a mesma imagem do Docker, pode ser mais eficiente automatizar passos usando uma ferramenta de gerenciamento de configuração como o Ansible ou o Chef. Para aprender mais sobre o gerenciamento de configuração, consulte Uma introdução ao gerenciamento de configuração e Automatizando a configuração do servidor com o Ansible: um kit de oficina da DigitalOcean.

      Em vez de compilar a mesma imagem em todos os hosts, você também pode simplificar a implantação usando um registro de imagem como o Docker Hub, que compila, armazena e distribui centralmente as imagens do Docker em vários servidores. Junto com um registro de imagem, um pipeline de integração e implantação contínuas pode ajudá-lo a compilar, testar e implantar imagens em seus servidores de aplicativo. Para mais informações sobre CI/CD, consulte Introdução a práticas recomendadas de CI/CD.



      Source link

      Масштабирование и обеспечение безопасности приложения Django с помощью Docker, Nginx и Let’s Encrypt


      Введение

      В облачных средах существует множество способов масштабирования и защиты приложения Django. Используя горизонтальное масштабирование и запустив несколько экземпляров приложения, вы можете создать более отказоустойчивую систему с высоким уровнем доступности, а также увеличить ее пропускную способность для одновременной обработки запросов. Одним из способов горизонтального масштабирования приложения Django является предоставление дополнительных серверов приложения, запускающих ваше приложение Django и его HTTP-сервер WSGI (например, Gunicorn или uWSGI). Чтобы направить и распределить входящие запросы через эту группу серверов приложения, вы можете использовать балансировщик нагрузки и обратный прокси-сервер, например Nginx. Nginx также может кэшировать статический контент и прерывать протокол безопасности Transport Layer Security (TLS), который используется для активации HTTPS и безопасных подключений к вашему приложению.

      Запуск приложения Django и прокси-сервера Nginx в контейнерах Docker гарантирует одинаковое поведение этих компонентов, независимо от среды развертывания. Кроме того, контейнеры предоставляют много возможностей, облегчающих упаковку и настройку вашего приложения.

      В этом обучающем руководстве вы выполните горизонтальное масштабирование контейнеризированного приложения Django и Gunicorn «Опросы», активировав два сервера приложения, каждый из которых будет выполнять экземпляр контейнера приложения Django и Gunicorn.

      Также вы активируете HTTPS, предоставив и настроив третий прокси-сервер, который будет выполнять запуск контейнера обратного прокси-сервера Nginx и контейнера клиента Certbot. Certbot будет предоставлять сертификаты TLS для Nginx от имени органа сертификации Let’s Encrypt. Это позволит вашему сайту получить высокий рейтинг безопасности от SSL Labs. Этот прокси-сервер будет получать все внешние запросы приложения и находиться перед двумя исходящими серверами приложения Django. И наконец, вы усилите данную распределенную систему, оставив внешний доступ только к прокси-серверу.

      Предварительные требования

      Для данного обучающего модуля вам потребуется следующее:

      • Три сервера Ubuntu 18.04:

        • Два сервера будут выполнять функции серверов приложения, используемых для запуска приложения Django и Gunicorn.
        • Один сервер будет выполнять роль прокси-сервера, используемого для запуска Nginx и Certbot.
        • Все должны иметь пользователя non-root user с привилегиями sudo и активный брандмауэр. Дополнительную информацию о настройке этих параметров см. в руководстве по первоначальной настройке сервера.
      • Установленный на всех трех серверах Docker. Инструкции по установке Docker см. в шагах 1 и 2 руководства Установка и использование Docker в Ubuntu 18.04.

      • Зарегистрированное доменное имя. В этом обучающем руководстве мы будем использовать your_domain.com. Вы можете получить бесплатный домен на Freenom или зарегистрировать доменное имя по вашему выбору.

      • Запись DNS A, где your_domain.com​​​ указывает на публичный IP-адрес вашего прокси-сервера. Вы можете воспользоваться введением в работу с DigitalOcean DNS, чтобы получить подробную информацию о добавлении доменов в учетную запись DigitalOcean, если вы используете этот способ.

      • Хранилище объектов S3, например пространство DigitalOcean, для хранения статических файлов проекта Django и набор ключей доступа к этому пространству. Чтобы узнать, как создать пространство, ознакомьтесь с документом Как создавать пространства. Чтобы узнать, как создать ключи доступа, ознакомьтесь со статьей Предоставление доступа к пространству с помощью ключей доступа. Внеся незначительные изменения, вы можете использовать любой сервис хранения, который поддерживает плагин django-storages.

      • Экземпляр сервера PostgreSQL, база данных и пользователь для приложения Django. Внеся незначительные изменения, вы можете использовать любую базу данных, которую поддерживает Django.

      Шаг 1 — Настройка первого сервера приложения Django

      Для начала мы клонируем репозиторий приложения Django на первый сервер приложения. Затем настроим и создадим образ приложения Docker, после чего протестируем приложение, запустив контейнер Django.

      Примечание. Если вы уже выполнили инструкции руководства Создание приложения Django и Gunicorn с помощью Docker, то вы уже выполнили шаг 1 и можете переходить к шагу 2, чтобы настроить второй сервер приложения.

      Начните с входа в первый из двух серверов приложения Django и используйте git для клонирования ветки polls-docker репозитория GitHub для приложения опросов Django Tutorial Polls App. В этом репозитории содержится код документации Django для образца приложения «Опросы». Ветка polls-docker содержит контейнеризированную версию приложения «Опросы». Чтобы узнать об изменениях, внесенных в приложение «Опросы» для эффективной работы в контейнеризированной среде, см. руководство Создание приложения Django и Gunicorn с помощью Docker.

      • git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git

      Перейдите в каталог django-polls:

      cd django-polls
      

      В этом каталоге содержится код Python приложения Django, Dockerfile, который Docker будет использовать для построения образа контейнера, а также файл env, содержащий список переменных среды для прохождения в рабочую среду контейнера. Проверьте файл Dockerfile с помощью cat:

      cat Dockerfile
      

      Output

      FROM python:3.7.4-alpine3.10 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 CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "mysite.wsgi"]

      Dockerfile использует официальный образ Docker Python 3.7.4 в качестве базы и устанавливает пакетные требования Python для Django и Gunicorn в соответствии с файлом django-polls/requirements.txt. Затем он удаляет некоторые ненужные файлы сборки, копирует код приложения в образ и устанавливает параметр выполнения PATH. И в заключение заявляет, что порт 8000 будет использоваться для принятия входящих подключений контейнера, и запускает gunicorn с тремя рабочими элементами, прослушивающими порт 8000.

      Дополнительную информацию о каждом этапе в Dockerfile см. в шаге 6 руководства Создание приложения Django и Gunicorn с помощью Docker.

      Теперь создайте образ с помощью docker build:

      Назовем образ polls, используя флаг -t, и передадим в текущий каталог как контекст сборки набор файлов для справки при построении образа.

      После того как Docker создаст и отметит образ, перечислите доступные образы, используя docker images:

      docker images
      

      Вы должны увидеть образ polls:

      Output

      REPOSITORY TAG IMAGE ID CREATED SIZE polls latest 80ec4f33aae1 2 weeks ago 197MB python 3.7.4-alpine3.10 f309434dea3a 8 months ago 98.7MB

      Перед запуском контейнера Django нам нужно настроить его рабочую среду с помощью файла env, присутствующего в текущем каталоге. Этот файл будет передан в команду docker run, которая используется для запуска контейнера, а Docker будет вводить настроенные переменные среды в рабочую среду контейнера.

      Откройте файл env с помощью nano или своего любимого редактора:

      nano env
      

      Мы настроим файл примерно так, и вам потребуется добавить некоторые дополнительные значения, как показано ниже.

      django-polls/env

      DJANGO_SECRET_KEY=
      DEBUG=True
      DJANGO_ALLOWED_HOSTS=
      DATABASE_ENGINE=postgresql_psycopg2
      DATABASE_NAME=polls
      DATABASE_USERNAME=
      DATABASE_PASSWORD=
      DATABASE_HOST=
      DATABASE_PORT=
      STATIC_ACCESS_KEY_ID=
      STATIC_SECRET_KEY=
      STATIC_BUCKET_NAME=
      STATIC_ENDPOINT_URL=
      DJANGO_LOGLEVEL=info
      

      Заполните недостающие значения для следующих ключей:

      • DJANGO_SECRET_KEY: задает уникальное, непрогнозируемое значение, как описано в документации Django. Один метод генерирования этого ключа представлен в разделе Изменение настроек приложения руководства Масштабируемое приложение Django.
      • DJANGO_ALLOWED_HOSTS: эта переменная защищает приложение и предотвращает атаки через заголовки хоста HTTP. С целью тестирования установите * как подстановочный символ, подходящий для всех хостов. В производственной среде вы должны задать значение your_domain.com. Дополнительную информацию об этой настройке Django см. в разделе Базовые настройки документации Django.
      • DATABASE_USERNAME: задает пользователя базы данных PostgreSQL, созданного на предварительных этапах.
      • DATABASE_NAME: задает polls или имя базы данных, созданной на предварительных этапах.
      • DATABASE_PASSWORD: задает пароль пользователя базы данных PostgreSQL, созданного на предварительных этапах.
      • DATABASE_HOST: задает имя хоста базы данных.
      • DATABASE_PORT: задает порт базы данных.
      • STATIC_ACCESS_KEY_ID: задает хранилище S3 или ключ доступа к пространству.
      • STATIC_SECRET_KEY: задает хранилище S3 или секретный ключ доступа к пространству.
      • STATIC_BUCKET_NAME: задает хранилище S3 или имя пространства.
      • STATIC_ENDPOINT_URL: задает соответствующее хранилище S3 или URL-адрес пространства, например https://space-name.nyc3.digitaloceanspaces.com, если ваше пространство находится в регионе nyc3.

      После завершения редактирования сохраните и закройте файл.

      Теперь мы будем использовать docker run, чтобы переопределить CMD, установленный в Dockerfile, и создать схему базы данных с помощью команд manage.py makemigrations и manage.py migrate:

      docker run --env-file env polls sh -c "python manage.py makemigrations && python manage.py migrate"
      

      Мы запускаем образ контейнера polls:latest, передаем в только что измененный файл переменной среды и переопределяем команду Dockerfile с помощью sh -c "python manage.py makemigrations && python manage.py migrate", которая создаст схему базы данных, определяемую кодом приложения. Если вы проводите запуск впервые, вы должны увидеть следующее:

      Output

      No changes detected Operations to perform: Apply all migrations: admin, auth, contenttypes, polls, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying polls.0001_initial... OK Applying sessions.0001_initial... OK

      Это означает, что схема базы данных успешно создана.

      Если в следующий раз вы запускаете migrate, Django будет выполнять холостую команду, если не изменить схему базы данных.

      Затем мы запустим еще один экземпляр контейнера приложения и используем внутри него интерактивную оболочку для создания административного пользователя проекта Django.

      docker run -i -t --env-file env polls sh
      

      Вы получите строку оболочки внутри работающего контейнера, которую сможете использовать для создания пользователя Django:

      python manage.py createsuperuser
      

      Введите имя пользователя, адрес электронной почты и пароль для пользователя, а после создания пользователя нажмите CTRL+D для выхода из контейнера и его удаления.

      В заключение мы создадим статические файлы приложения и загрузим их в пространство DigitalOcean с помощью collectstatic. Обратите внимание, что для завершения процесса может потребоваться время.

      docker run --env-file env polls sh -c "python manage.py collectstatic --noinput"
      

      После создания и загрузки файлов вы получите следующий вывод.

      Output

      121 static files copied.

      Теперь мы можем запустить приложение:

      docker run --env-file env -p 80:8000 polls
      

      Output

      [2019-10-17 21:23:36 +0000] [1] [INFO] Starting gunicorn 19.9.0 [2019-10-17 21:23:36 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1) [2019-10-17 21:23:36 +0000] [1] [INFO] Using worker: sync [2019-10-17 21:23:36 +0000] [7] [INFO] Booting worker with pid: 7 [2019-10-17 21:23:36 +0000] [8] [INFO] Booting worker with pid: 8 [2019-10-17 21:23:36 +0000] [9] [INFO] Booting worker with pid: 9

      Мы запустим команду по умолчанию, определенную в Dockerfile, gunicorn --bind :8000 --workers 3 mysite.wsgi:application и раскроем порт контейнера 8000, чтобы сопоставить порт 80 на сервере Ubuntu с портом 8000 контейнера polls.

      Теперь вы сможете перейти к приложению polls с использованием веб-браузера, набрав в адресной-строке URL http://APP_SERVER_1_IP. Поскольку маршрут для пути / не определен, скорее всего, вы получите ошибку 404 Страница не найдена.

      Предупреждение. При использовании с Docker брандмауэра UFW, Docker обходит любые настроенные правила брандмауэра UFW. Об этой ситуации можно прочитать в GitHub. Это объясняет, почему у вас есть доступ к порту 80 сервера, хотя вы не создавали правило доступа UFW на предварительном этапе. На шаге 5 мы рассмотрим эту брешь в системе безопасности и скорректируем настройки UFW. Если вы не используете UFW, а используете облачные брандмауэры DigitalOcean, вы можете спокойно игнорировать это предупреждение.

      Перейдите в адрес http://APP_SERVER_1_IP/polls, чтобы увидеть интерфейс приложения «Опросы»:

      Интерфейс приложения «Опросы»

      Чтобы посмотреть административный интерфейс, перейдите по адресу http://APP_SERVER_1_IP/admin. Вы должны увидеть окно аутентификации администратора приложения «Опросы»:

      Страница аутентификации администратора приложения

      Введите имя администратора и пароль, которые вы создали с помощью команды createsuperuser.

      После аутентификации вы сможете получить доступ к административному интерфейсу приложения «Опросы»:

      Основной интерфейс администратора приложения

      Обратите внимание, что статические активы приложений admin и polls поступают напрямую из хранилища объекта. Чтобы убедиться в этом, ознакомьтесь с материалами Тестирование доставки статических файлов пространства.

      После завершения изучения данных нажмите CTRL+C в окне терминала, где запущен контейнер Docker, чтобы удалить контейнер.

      Когда мы убедились, что контейнер приложения работает, как и ожидалось, можно запустить его в раздельном режиме, при котором контейнер будет работать в фоновом режиме, что позволит вам выйти из сеанса SSH.

      docker run -d --rm --name polls --env-file env -p 80:8000 polls
      

      Флаг -d предписывает Docker запустить контейнер в раздельном режиме, флаг -rm очищает файловую систему контейнера после выхода контейнера, а мы называем контейнер polls.

      Выйдите из первого сервера приложения Django и перейдите к адресу http://APP_SERVER_1_IP/polls для подтверждения того, что контейнер работает корректно.

      После запуска и активации первого сервера приложения Django вы можете настроить второй сервер приложения Django.

      Шаг 2 — Настройка второго сервера приложения Django

      Поскольку многие команды для настройки сервера будут выглядеть так же, как и в предыдущем шаге, мы представим их здесь в сокращенном виде. Дополнительную информацию о каждой команде из этого этапа можно найти в шаге 1.

      Начните с входа на второй сервер приложения Django.

      Клонируйте ветку polls-docker репозитория GitHub django-polls:

      • git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git

      Перейдите в каталог django-polls:

      cd django-polls
      

      Создайте образ с помощью docker build:

      Откройте файл env с помощью nano или своего любимого редактора:

      nano env
      

      django-polls/env

      DJANGO_SECRET_KEY=
      DEBUG=True
      DJANGO_ALLOWED_HOSTS=
      DATABASE_ENGINE=postgresql_psycopg2
      DATABASE_NAME=polls
      DATABASE_USERNAME=
      DATABASE_PASSWORD=
      DATABASE_HOST=
      DATABASE_PORT=
      STATIC_ACCESS_KEY_ID=
      STATIC_SECRET_KEY=
      STATIC_BUCKET_NAME=
      STATIC_ENDPOINT_URL=
      DJANGO_LOGLEVEL=info
      

      Заполните недостающие значения, как показано в шаге 1. После завершения редактирования сохраните и закройте файл.

      Наконец, запустите контейнер приложения в раздельном режиме:

      docker run -d --rm --name polls --env-file env -p 80:8000 polls
      

      Перейдите на http://APP_SERVER_2_IP/polls для подтверждения того, что контейнер работает корректно. Вы можете безопасно выйти из второго сервера приложения без завершения работы контейнера.

      Когда вы настроили и запустили оба контейнера приложения Django, можно переходить к настройке обратного прокси-сервера Nginx.

      Шаг 3 — Настройка контейнера Docker Nginx

      Nginx — это универсальный веб-сервер, предлагающий ряд функций, включая обратное проксирование, балансирование нагрузки и кэширование. В этом обучающем руководстве мы вызгрузили статические активы Django в хранилище объектов, поэтому мы не будем использовать возможности кэширования Nginx. Однако мы будем использовать Nginx как обратный прокси-сервер для двух серверов приложения Django и распределим входящие запросы между ними. Кроме того, Nginx будет осуществлять прекращение TLS и перенаправление с помощью сертификата TLS, предоставленного Certbot. Это означает, что он заставит клиентов использовать HTTPS, перенаправляя входящие запросы HTTP в порт 443. Затем он расшифрует запросы HTTPS и передаст их на серверы Django.

      В этом обучающем руководстве мы приняли проектное решение отделить контейнеры Nginx от серверов. В зависимости от своих потребностей вы можете запускать контейнер Nginx на одном из серверов приложения Django, обрабатывая запросы с помощью прокси-сервера локально или на другом сервере Django. Еще один вариант архитектуры: запуск двух контейнеров Nginx, по одному на каждом сервере, и облачного балансировщика нагрузки перед ними. Каждая архитектура имеет свои преимущества безопасности и производительности, и вам потребуется провести нагрузочное тестирование, чтобы определить узкие места. Гибкая архитектура, описанная в данном руководстве, позволяет масштабировать как серверный слой приложения Django, так и прокси-сервер Nginx. Если узким местом станет единственный контейнер Nginx, вы сможете масштабировать несколько прокси-серверов Nginx и добавить облачный балансировщик нагрузок или быстрый балансировщик нагрузок L4, например HAProxy.

      Когда вы создадите и запустите оба сервера приложения Django, можно начинать настройку прокси-сервера Nginx. Войдите на ваш прокси-сервер и создайте каталог с именем conf:

      mkdir conf
      

      Создайте файл конфигурации nginx.conf с помощью nano или своего любимого редактора:

      nano conf/nginx.conf
      

      Вставьте следующую конфигурацию Nginx:

      conf/nginx.conf

      
      upstream django {
          server APP_SERVER_1_IP;
          server APP_SERVER_2_IP;
      }
      
      server {
          listen 80 default_server;
          return 444;
      }
      
      server {
          listen 80;
          listen [::]:80;
          server_name your_domain.com;
          return 301 https://$server_name$request_uri;
      }
      
      server {
          listen 443 ssl http2;
          listen [::]:443 ssl http2;
          server_name your_domain.com;
      
          # SSL
          ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
          ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
      
          ssl_session_cache shared:le_nginx_SSL:10m;
          ssl_session_timeout 1440m;
          ssl_session_tickets off;
      
          ssl_protocols TLSv1.2 TLSv1.3;
          ssl_prefer_server_ciphers off;
      
          ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
      
          client_max_body_size 4G;
          keepalive_timeout 5;
      
              location / {
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
                proxy_set_header Host $http_host;
                proxy_redirect off;
                proxy_pass http://django;
              }
      
          location ^~ /.well-known/acme-challenge/ {
              root /var/www/html;
          }
      
      }
      

      Блоки upstream, server и location настраивают Nginx таким образом, чтобы перенаправлять HTTP-запросы на HTTPS и распределять нагрузку на два сервера приложения Django, настроенных в шагах 1 и 2. Дополнительную информацию о структуре файла конфигурации Nginx см. в статье Понимание структуры файла конфигурации и контекста конфигурации. Также может быть полезна статья Понимание работы сервера Nginx и алгоритмов выбора блоков расположения.

      Эта конфигурация была собрана из примеров файлов конфигурации из Gunicorn, Certbot и Nginx и является минимальной конфигурацией Nginx, необходимой для построения и запуска данной архитектуры. Настройка данной конфигурации Nginx выходит за рамки этой статьи, но вы можете использовать такой инструмент, как NGINXConfig, для генерирования исполнительных и безопасных файлов конфигурации для вашей архитектуры.

      Блок upstream определяет группу серверов, которые используются для передачи запросов с использованием директивы proxy_pass:

      conf/nginx.conf

      upstream django {
          server APP_SERVER_1_IP;
          server APP_SERVER_2_IP;
      }
      . . .
      

      В этом блоке мы используем имя django и включаем IP-адреса обоих серверов приложения Django. Если серверы приложения работают на DigitalOcean с активированной сетью VPC, вам следует использовать их частные IP-адреса. Чтобы узнать, как активировать сеть VPC на DigitalOcean, см. Как активировать сеть VPC на существующих дроплетах.

      Первый блок server захватывает запросы, которые не соответствуют вашему домену, и прерывает соединение. Например, в этом блоке будет обрабатываться прямой запрос HTTP на IP-адрес вашего сервера:

      conf/nginx.conf

      . . .
      server {
          listen 80 default_server;
          return 444;
      }
      . . .
      

      Следующий блок server перенаправляет запросы HTTP на ваш домен на HTTPS, используя перенаправление HTTP 301. Эти запросы рассматриваются в последнем блоке server:

      conf/nginx.conf

      . . .
      server {
          listen 80;
          listen [::]:80;
          server_name your_domain.com;
          return 301 https://$server_name$request_uri;
      }
      . . .
      

      Эти две директивы определяют путь к сертификату TLS и секретному ключу. Они предоставляются с помощью Certbot и монтируются в контейнер Nginx на следующем шаге.

      conf/nginx.conf

      . . .
      ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
      ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
      . . .
      

      Это по умолчанию параметры безопасности SSL, рекомендованные Certbot. Дополнительную информацию о них см. в разделе Модуль ngx_http_ssl_module в документации Nginx. Еще одним полезным руководством, которое вы можете использовать для настройки конфигурации SSL, является инструкция по безопасности от Mozilla Security/Server Side TLS.

      conf/nginx.conf

      . . .
          ssl_session_cache shared:le_nginx_SSL:10m;
          ssl_session_timeout 1440m;
          ssl_session_tickets off;
      
          ssl_protocols TLSv1.2 TLSv1.3;
          ssl_prefer_server_ciphers off;
      
          ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
      . . .
      

      Эти две директивы из примерной конфигурации Nginx от Gunicorn определяют максимальный разрешенный размер тела запроса клиента и назначают временной интервал поддержки соединения с клиентом. Nginx закроет соединения с клиентом после keepalive_timeout.

      conf/nginx.conf

      . . .
      client_max_body_size 4G;
      keepalive_timeout 5;
      . . .
      

      Первый блок location предписывает Nginx направлять запросы на серверы upstream django через HTTP. Также он сохраняет клиентские заголовки HTTP, захватывающие исходный IP-адрес, протокол, используемый для соединения, и целевой хост:

      conf/nginx.conf

      . . .
      location / {
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header X-Forwarded-Proto $scheme;
          proxy_set_header Host $http_host;
          proxy_redirect off;
          proxy_pass http://django;
      }
      . . .
      

      Дополнительную информацию об этих директивах см. в разделах развертывание Gunicorn и Модуль ngx_http_proxy_module из документации Nginx.

      Последний блок location захватывает запросы на путь /well-known/acme-challenge/, используемый Certbot для задач HTTP-01 по проверке вашего домена с помощью Let’s Encrypt и предоставлению или обновлению сертификатов TLS. Дополнительную информацию о задаче HTTP-01, используемой Certbot, см. в разделе Виды задач из документации Let’s Encrypt.

      conf/nginx.conf

      . . .
      location ^~ /.well-known/acme-challenge/ {
              root /var/www/html;
      }
      

      После завершения редактирования сохраните и закройте файл.

      Теперь вы можете использовать этот файл конфигурации для запуска контейнера Nginx Docker. В этом обучающем руководстве мы будем использовать образ nginx:1.19.0, версия 1.19.0 официального образа Docker, обслуживаемого Nginx.

      При первом запуске контейнера Nginx будет выдавать ошибку и отказывать в работе, так как мы еще не предоставили сертификаты, определенные в файле конфигурации. Однако мы все равно будем выполнять команду для локальной загрузки образа Nginx и протестируем корректность работы остальных элементов.

      docker run --rm --name nginx -p 80:80 -p 443:443 
          -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro 
          -v /var/www/html:/var/www/html 
          nginx:1.19.0
      

      Давайте назовем контейнер nginx и свяжем порты хоста 80 и 443 с соответствующими портами контейнера. Флаг -v монтирует файл конфигурации в контейнер Nginx на /etc/nginx/conf.d/nginx.conf, который образ Nginx предварительно настраивает для загрузки. Он монтируется в режиме ro или «только для чтения», поэтому контейнер не может изменять файл. Также в контейнер монтируется корневой веб-каталог /var/www/html. Наконец, nginx:1.19.0 предписывает Docker вытащить и запустить образ nginx:1.19.0 из Dockerhub.

      Docker вытащит и запустит образ, затем Nginx выдаст ошибку, т.к. не найдет настроенный сертификат TLS и секретный ключ. На следующем шаге мы предоставим их с помощью клиента Dockerized Certbot и органа сертификации Let’s Encrypt.

      Шаг 4 — Настройка Certbot и обновление сертификата Let’s Encrypt

      Certbot — это клиент Let’s Encrypt, разработанный фондом Electronic Frontier Foundation. Он предоставляет бесплатные сертификаты TLS от органа сертификации Let’s Encrypt, которые позволяют браузерам идентифицировать ваши веб-серверы. Поскольку мы установили Docker на нашем прокси-сервере Nginx, мы будем использовать образ Docker Certbot для предоставления и обновления сертификатов

      Для начала проверьте наличие записи А DNS, соединенной с публичным IP-адресом прокси-сервера. Затем на вашем прокси-сервере представьте тестовую версию сертификатов с помощью образа Docker certbot:

      docker run -it --rm -p 80:80 --name certbot 
               -v "/etc/letsencrypt:/etc/letsencrypt" 
               -v "/var/lib/letsencrypt:/var/lib/letsencrypt" 
               certbot/certbot certonly --standalone --staging -d your_domain.com
      

      Эта команда запускает образ Docker certbot в интерактивном режиме и направляет порт 80 на хосте на порт контейнера 80. Она создает и монтирует два каталога хоста в контейнер: /etc/letsencrypt/ и /var/lib/letsencrypt/. certbot работает в режиме standalone без Nginx и будет использовать серверы staging Let’s Encrypt для валидации домена.

      Когда появится строка ввода, укажите свой электронный адрес и примите Условия обслуживания. Если валидация домена прошла успешно, вы увидете следующий вывод:

      Output

      Obtaining a new certificate Performing the following challenges: http-01 challenge for stubb.dev Waiting for verification... Cleaning up challenges IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/your_domain.com/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/your_domain.com/privkey.pem Your cert will expire on 2020-09-15. To obtain a new or tweaked version of this certificate in the future, simply run certbot again. To non-interactively renew *all* of your certificates, run "certbot renew" - Your account credentials have been saved in your Certbot configuration directory at /etc/letsencrypt. You should make a secure backup of this folder now. This configuration directory will also contain certificates and private keys obtained by Certbot so making regular backups of this folder is ideal.

      Вы можете проверить сертификат с помощью cat:

      sudo cat /etc/letsencrypt/live/your_domain.com/fullchain.pem
      

      После предоставления сертификата TLS мы можем протестировать конфигурацию Nginx, собранную на предыдущем шаге:

      docker run --rm --name nginx -p 80:80 -p 443:443 
          -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro 
          -v /etc/letsencrypt:/etc/letsencrypt 
          -v /var/lib/letsencrypt:/var/lib/letsencrypt 
          -v /var/www/html:/var/www/html 
          nginx:1.19.0
      

      Задаем те же команды, что и на шаге 3, добавив оба только что созданных каталога Let’s Encrypt.

      После установки и активации Nginx перейдите на http://your_domain.com. Вы можете получить предупреждение в браузере о том, что орган сертификации недействителен. Это ожидаемо, так как мы предоставили тестовые, а не рабочие сертификаты Let’s Encrypt. Проверьте строку URL-адреса вашего браузера, чтобы убедиться, что ваш запрос HTTP был перенаправлен на HTTPS.

      Нажмите CTRL+C на вашем терминале, чтобы выйти из Nginx, и снова запустите клиент certbot, на этот раз опустив флаг --staging:

      docker run -it --rm -p 80:80 --name certbot 
               -v "/etc/letsencrypt:/etc/letsencrypt" 
               -v "/var/lib/letsencrypt:/var/lib/letsencrypt" 
               certbot/certbot certonly --standalone -d your_domain.com
      

      Когда появится окно выбора: сохранить действующий сертификат или обновить и заменить, нажмите 2, чтобы обновить, а затем ENTER, чтобы подтвердить свой выбор.

      После предоставления рабочего сертификата TLS запустите сервер Nginx снова:

      docker run --rm --name nginx -p 80:80 -p 443:443 
          -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro 
          -v /etc/letsencrypt:/etc/letsencrypt 
          -v /var/lib/letsencrypt:/var/lib/letsencrypt 
          -v /var/www/html:/var/www/html 
          nginx:1.19.0
      

      В вашем браузере перейдите на http://your_domain.com. В строке URL-адреса подтвердите, что запрос HTTP был перенаправлен на HTTPS. Поскольку приложение «Опросы» не имеет настроенного по умолчанию маршрута, вы увидите ошибку Django Страница не найдена. Перейдите на https://your_domain.com/polls и вы увидите стандартный интерфейс приложения «Опросы»:

      Интерфейс приложения «Опросы»

      На этом этапе вы предоставили рабочий сертификат TLS с помощью клиента Docker Certbot, направляете внешние запросы с помощью обратного прокси-сервера и распределяете нагрузку между двумя серверами приложения Django.

      Срок действия сертификатов Let’s Encrypt истекает каждые 90 дней. Чтобы удостовериться в действительности вашего сертификата, необходимо регулярно обновлять его до истечения срока действия. После запуска Nginx вы должны использовать клиент Certbot в режиме webroot вместо режима standalone. Это означает, что Certbot будет выполнять проверку путем создания файла в каталоге /var/www/html/.well-known/acme-challenge/, и запросы Let’s Encrypt о валидации на этом пути будут захватываться правилом location, определенном в конфигурации Nginx на шаге 3. Certbot будет менять сертификаты, и вы сможете перезагрузить Nginx, чтобы он использовал уже новый предоставленный сертификат.

      Существует множество способов автоматизации этой процедуры, но автоматическое обновление сертификатов TLS выходит за рамки данного обучающего руководства. Для аналогичного процесса с помощью утилиты планирования cron см. шаг 6 руководства Обеспечение безопасности контейнеризированного приложения Node.js с помощью Nginx, Let’s Encrypt и Docker Compose.

      На вашем терминале нажмите CTRL+C, чтобы удалить контейнер Nginx. Запустите его снова в раздельном режиме, добавив флаг -d:

      docker run --rm --name nginx -d -p 80:80 -p 443:443 
          -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro 
          -v /etc/letsencrypt:/etc/letsencrypt 
          -v /var/lib/letsencrypt:/var/lib/letsencrypt 
        -v /var/www/html:/var/www/html 
          nginx:1.19.0
      

      При запущенном в фоновом режиме Nginx используйте следующую команду для выполнения сухого запуска процедуры обновления сертификата:

      docker run -it --rm --name certbot 
          -v "/etc/letsencrypt:/etc/letsencrypt" 
        -v "/var/lib/letsencrypt:/var/lib/letsencrypt" 
        -v "/var/www/html:/var/www/html" 
        certbot/certbot renew --webroot -w /var/www/html --dry-run
      

      Мы используем плагин --webroot, указываем корневой путь и используем флаг –--dry-run, чтобы убедиться, что все работает корректно без обновления сертификата.

      Если моделирование обновления завершится успешно, вы увидите следующий вывод:

      Output

      Cert not due for renewal, but simulating renewal for dry run Plugins selected: Authenticator webroot, Installer None Renewing an existing certificate Performing the following challenges: http-01 challenge for your_domain.com Using the webroot path /var/www/html for all unmatched domains. Waiting for verification... Cleaning up challenges - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - new certificate deployed without reload, fullchain is /etc/letsencrypt/live/your_domain.com/fullchain.pem - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates below have not been saved.) Congratulations, all renewals succeeded. The following certs have been renewed: /etc/letsencrypt/live/your_domain.com/fullchain.pem (success) ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates above have not been saved.) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      В рабочей настройке после обновления сертификата вы должны перезагрузить Nginx, чтобы изменения вступили в силу. Чтобы перезагрузить Nginx, запустите следующую команду:

      docker kill -s HUP nginx
      

      Эта команда будет отправлять сигнал Unix HUP для процесса Nginx, работающего внутри контейнера Docker nginx. После получения этого сигнала Nginx перезагрузит конфигурацию и обновленные сертификаты.

      После активации HTTPS и всех компонентов данной архитектуры итоговым шагом является блокировка настройки от внешнего доступа к двум серверам приложения. Все запросы HTTP должны проходить через прокси-сервер Nginx.

      Шаг 5 — Предотвращение внешнего доступа к серверу приложения Django

      В описанной в этом руководстве архитектуре прекращение SSL происходит на прокси-сервере Nginx. Это означает, что Nginx расшифровывает соединение SSL, а пакеты передаются на серверы приложения Django без шифрования. Для многих случаев использования этого уровня безопасности достаточно. Для приложений, содержащих финансовые данные или данные о состоянии здоровья, вам может понадобится внедрение сквозного шифрования. Вы можете сделать это, передавая зашифрованные пакеты через балансировщик нагрузки и расшифровывая их на серверах приложения, или зашифровывая на прокси-сервере и снова расшифровывая на серверах приложения Django. Эти методы выходят за рамки данного руководства, но для получения более подробной информации можно ознакомиться со статьей Сквозное шифрование.

      Прокси-сервер Nginx действует как шлюз между внешним трафиком и внутренней сетью. Теоретически ни один внешний клиент не должен иметь прямого доступа к внутренним серверам приложения, и все запросы должны проходить через сервер Nginx. В примечании на шаге 1 кратко описывается проблема с Docker, где Docker обходит настройки брандмауэра по умолчанию ufw и открывает порты внешним способом, что может быть небезопасным. Чтобы решить эту проблему безопасности, рекомендуется использовать облачные брандмауэры при работе с серверами на базе Docker. Дополнительную информацию о создании облачного брандмауэра с помощью DigitalOcean можно найти в руководстве Создание брандмауэра. Также вы можете использовать непосредственно iptables вместо ufw. Дополнительную информацию об использовании iptables с Docker можно найти в статье Docker and iptables.

      На этом шаге мы изменим конфигурацию UFW, чтобы заблокировать внешний доступ к портам хоста, которые открывает Docker. При запуске Django на серверах приложения мы передаем флаг -p 80:8000 на docker, который направляет порт 80 на хосте в порт контейнера 8000. Это также открывает порт 80 для внешних клиентов, которых вы можете верифицировать, посетив http://your_app_server_1_IP. Чтобы предотвратить прямой доступ, мы изменим конфигурацию UFW с помощью метода, описанного в репозитории GitHub ufw-docker.

      Начните с входа на первый сервер приложения Django. Затем откройте файл /etc/ufw/after.rules с привилегиями суперпользователя, используя nano или свой любимый редактор:

      sudo nano /etc/ufw/after.rules
      

      Введите свой пароль при запросе и нажмите ENTER для подтверждения.

      Вы должны увидеть следующие правила ufw:

      /etc/ufw/after.rules

      #
      # rules.input-after
      #
      # Rules that should be run after the ufw command line added rules. Custom
      # rules should be added to one of these chains:
      #   ufw-after-input
      #   ufw-after-output
      #   ufw-after-forward
      #
      
      # Don't delete these required lines, otherwise there will be errors
      *filter
      :ufw-after-input - [0:0]
      :ufw-after-output - [0:0]
      :ufw-after-forward - [0:0]
      # End required lines
      
      # don't log noisy services by default
      -A ufw-after-input -p udp --dport 137 -j ufw-skip-to-policy-input
      -A ufw-after-input -p udp --dport 138 -j ufw-skip-to-policy-input
      -A ufw-after-input -p tcp --dport 139 -j ufw-skip-to-policy-input
      -A ufw-after-input -p tcp --dport 445 -j ufw-skip-to-policy-input
      -A ufw-after-input -p udp --dport 67 -j ufw-skip-to-policy-input
      -A ufw-after-input -p udp --dport 68 -j ufw-skip-to-policy-input
      
      # don't log noisy broadcast
      -A ufw-after-input -m addrtype --dst-type BROADCAST -j ufw-skip-to-policy-input
      
      # don't delete the 'COMMIT' line or these rules won't be processed
      COMMIT
      

      Прокрутите вниз и вставьте следующий блок правил конфигурации UFW:

      /etc/ufw/after.rules

      . . .
      
      # BEGIN UFW AND DOCKER
      *filter
      :ufw-user-forward - [0:0]
      :DOCKER-USER - [0:0]
      -A DOCKER-USER -j RETURN -s 10.0.0.0/8
      -A DOCKER-USER -j RETURN -s 172.16.0.0/12
      -A DOCKER-USER -j RETURN -s 192.168.0.0/16
      
      -A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN
      
      -A DOCKER-USER -j ufw-user-forward
      
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12
      
      -A DOCKER-USER -j RETURN
      COMMIT
      # END UFW AND DOCKER
      

      Эти правила ограничивают публичный доступ к портам, которые открывает Docker, и позволяют получить доступ от частных диапазонов IP-диапазонов 10.0.0.0/8, 172.16.0.0/12 и 192.168.0.0/16. Если вы используете VPC с DigitalOcean, тогда дроплеты в вашей сети VPC получат доступ к открытому порту через закрытый интерфейс, а внешние клиенты не получат. Дополнительную информацию о VPC см. в официальной документации VPC. Дополнительную информацию о правилах, которые использованы в этом фрагменте, ищите в разделе Как это работает? инструкции ufw-docker README.

      Если вы не используете VPC с Digitalocean и ввели публичные IP-адреса серверов приложений в блоке upstream вашей конфигурации Nginx, вам придется изменить брандмауэр UFW, чтобы разрешить трафик с сервера Nginx через порт 80 на серверы приложения Django. Инструкции по созданию правил allow с брандмауэром UFW см. в руководстве Основы UFW: общие правила и команды брандмауэра.

      После завершения редактирования сохраните и закройте файл.

      Перезапустите ufw для выбора новой конфигурации:

      sudo systemctl restart ufw
      

      Перейдите на http://APP_SERVER_1_IP в вашем браузере для подтверждения того, что вы больше не можете получить доступ к серверу приложения через порт 80.

      Повторите этот процесс на втором сервере приложения Django.

      Выйдите из первого сервера приложения или откройте другое окно терминала и войдите на второй сервер приложения Django. Затем откройте файл /etc/ufw/after.rules с привилегиями суперпользователя, используя nano или свой любимый редактор:

      sudo nano /etc/ufw/after.rules
      

      Введите свой пароль при запросе и нажмите ENTER для подтверждения.

      Прокрутите вниз и вставьте следующий блок правил конфигурации UFW:

      /etc/ufw/after.rules

      . . .
      
      # BEGIN UFW AND DOCKER
      *filter
      :ufw-user-forward - [0:0]
      :DOCKER-USER - [0:0]
      -A DOCKER-USER -j RETURN -s 10.0.0.0/8
      -A DOCKER-USER -j RETURN -s 172.16.0.0/12
      -A DOCKER-USER -j RETURN -s 192.168.0.0/16
      
      -A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN
      
      -A DOCKER-USER -j ufw-user-forward
      
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12
      
      -A DOCKER-USER -j RETURN
      COMMIT
      # END UFW AND DOCKER
      

      После завершения редактирования сохраните и закройте файл.

      Перезапустите ufw для выбора новой конфигурации:

      sudo systemctl restart ufw
      

      Перейдите на http://APP_SERVER_2_IP в вашем браузере для подтверждения того, что вы больше не можете получить доступ к серверу приложения через порт 80.

      В заключение перейдите на https://your_domain_here/polls, чтобы подтвердить, что прокси-сервер Nginx все еще имеет доступ к серверам Django. Вы должны увидеть интерфейс приложения «Опросы» по умолчанию.

      Заключение

      В этом обучающем руководстве мы настроили масштабируемое приложение Django «Опросы» при помощи контейнеров Docker. По мере увеличения трафика и загрузки системы вы можете масштабировать каждый слой отдельно: слой прокси-сервера Nginx, слой серверного приложения Django и слой базы данных PostgreSQL.

      При построении распределенной системы вам часто приходится выбирать из нескольких проектных решений и архитектур. Архитектура, описанная в этом руководстве, является гибким планом для разработки масштабируемых приложений с Django и Docker.

      Вам бы хотелось контролировать поведение контейнеров при возникновении ошибок или автоматически запускать контейнеры во время загрузки системы. Для этого вы можете использовать диспетчер процесса, например Systemd, или применять политики перезапуска. Дополнительную информацию можно прочитать в разделе Автоматический запуск контейнера из документации Docker.

      При работе в масштабе с несколькими хостами на одном и том же образе Docker более эффективной может стать автоматизация шагов с использованием инструмента управления конфигурацией, например Ansible или Chef. Дополнительную информацию об управлении конфигурацией см. в руководствах Введение в управление конфигурацией и Автоматизированная настройка сервера с помощью Ansible: набор материалов для тренинга DigitalOcean.

      Вместо создания такого же образа на каждом хосте вы можете также упорядочить развертывание с помощью реестра образов, такого как Docker Hub, который централизованно строит, хранит и распределяет образы Docker на несколько серверов. Наряду с реестром образов непрерывный конвейер интеграции и развертывания может помочь вам строить, тестировать и развертывать образы на ваших серверах приложения. Дополнительную информацию о CI/CD см. в руководстве Введение в передовой опыт CI/CD.



      Source link

      How To Scale and Secure a Django Application with Docker, Nginx, and Let’s Encrypt


      Introduction

      In cloud-based environments, there are multiple ways to scale and secure a Django application. By scaling horizontally, and running several copies of your app, you can build a more fault-tolerant and highly-available system, while also increasing its throughput so that requests can be processed simultaneously. One way to horizontally scale a Django app is to provision additional app servers that run your Django application and its WSGI HTTP server (like Gunicorn or uWSGI). To route and distribute incoming requests across this set of app servers, you can use a load balancer and reverse proxy like Nginx. Nginx can also cache static content and terminate Transport Layer Security (TLS) connections, used to provide HTTPS and secure connections to your app.

      Running your Django application and Nginx proxy inside of Docker containers ensures that these components behave the same way regardless of the environment they are deployed into. In addition, containers provide many features that facilitate packaging and configuring your application.

      In this tutorial, you’ll horizontally scale a containerized Django and Gunicorn Polls application by provisioning two application servers that will each run a copy of a Django and Gunicorn app container.

      You’ll also enable HTTPS by provisioning and configuring a third proxy server that will run an Nginx reverse proxy container and a Certbot client container. Certbot will provision TLS certificates for Nginx from the Let’s Encrypt certificate authority. This will ensure that your site receives a high security rating from SSL Labs. This proxy server will receive all of your app’s external requests and sit in front of the two upstream Django application servers. Finally, you’ll harden this distributed system by restricting external access to only the proxy server.

      Prerequisites

      To follow this tutorial, you will need:

      • Three Ubuntu 18.04 servers:

        • Two servers will be application servers, used to run your Django and Gunicorn app.
        • One server will be a proxy server, used to run Nginx and Certbot.
        • All should have a non-root user with sudo privileges, and an active firewall. For guidance on how to set these up, please see this Initial Server Setup guide.
      • Docker installed on all three servers. For guidance on installing Docker, follow Steps 1 and 2 of How To Install and Use Docker on Ubuntu 18.04.

      • A registered domain name. This tutorial will use your_domain.com throughout. You can get one for free at Freenom, or use the domain registrar of your choice.

      • An A DNS record with your_domain.com pointing to your proxy server’s public IP address. You can follow this introduction to DigitalOcean DNS for details on how to add it to a DigitalOcean account, if that’s what you’re using.

      • An S3 object storage bucket such as a DigitalOcean Space to store your Django project’s static files and a set of Access Keys for this Space. To learn how to create a Space, consult the How to Create Spaces product documentation. To learn how to create Access Keys for Spaces, consult Sharing Access to Spaces with Access Keys. With minor changes, you can use any object storage service that the django-storages plugin supports.

      • A DigitalOcean Managed PostgreSQL cluster. To learn how to create a cluster, consult the DigitalOcean Managed Databases product documentation. With minor changes, you can use any database that Django supports.

        • A PostgreSQL database called polls (or another memorable name to input in your config files below) and user for your Django app. For guidance on creating these, follow Step 1 of How to Build a Django and Gunicorn Application with Docker. You can perform these steps from any of the three servers.

      Step 1 — Configuring the First Django Application Server

      To begin, we’ll clone the Django application repository onto the first app server. Then, we’ll configure and build the application Docker image, and test the application by running the Django container.

      Note: If you’re continuing from How to Build a Django and Gunicorn Application with Docker, you will have already completed Step 1 and can skip ahead to Step 2 to configure the second app server.

      Start by logging in to the first of the two Django application servers and using git to clone the polls-docker branch of the Django Tutorial Polls App GitHub repository. This repo contains code for the Django documentation’s sample Polls application. The polls-docker branch contains a Dockerized version of the Polls app. To learn how the Polls app was modified to work effectively in a containerized environment, please see How to Build a Django and Gunicorn Application with Docker.

      • git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git

      Navigate into the django-polls directory:

      cd django-polls
      

      This directory contains the Django application Python code, a Dockerfile that Docker will use to build the container image, as well as an env file that contains a list of environment variables to be passed into the container’s running environment. Inspect the Dockerfile using cat:

      cat Dockerfile
      

      Output

      FROM python:3.7.4-alpine3.10 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 CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "mysite.wsgi"]

      This Dockerfile uses the official Python 3.7.4 Docker image as a base, and installs Django and Gunicorn’s Python package requirements, as defined in the django-polls/requirements.txt file. It then removes some unnecessary build files, copies the application code into the image, and sets the execution PATH. Finally, it declares that port 8000 will be used to accept incoming container connections, and runs gunicorn with 3 workers, listening on port 8000.

      To learn more about each of the steps in this Dockerfile, please see Step 6 of How to Build a Django and Gunicorn Application with Docker.

      Now, build the image using docker build:

      We name the image polls using the -t flag and pass in the current directory as a build context, the set of files to reference when constructing the image.

      After Docker builds and tags the image, list available images using docker images:

      docker images
      

      You should see the polls image listed:

      Output

      REPOSITORY TAG IMAGE ID CREATED SIZE polls latest 80ec4f33aae1 2 weeks ago 197MB python 3.7.4-alpine3.10 f309434dea3a 8 months ago 98.7MB

      Before we run the Django container, we need to configure its running environment using the env file present in the current directory. This file will be passed into the docker run command used to run the container, and Docker will inject the configured environment variables into the container’s running environment.

      Open the env file with nano or your favorite editor:

      nano env
      

      We’ll be configuring the file like so, and you’ll need to add some additional values as outlined below.

      django-polls/env

      DJANGO_SECRET_KEY=
      DEBUG=True
      DJANGO_ALLOWED_HOSTS=
      DATABASE_ENGINE=postgresql_psycopg2
      DATABASE_NAME=polls
      DATABASE_USERNAME=
      DATABASE_PASSWORD=
      DATABASE_HOST=
      DATABASE_PORT=
      STATIC_ACCESS_KEY_ID=
      STATIC_SECRET_KEY=
      STATIC_BUCKET_NAME=
      STATIC_ENDPOINT_URL=
      DJANGO_LOGLEVEL=info
      

      Fill in the missing values for the following keys:

      • DJANGO_SECRET_KEY: Set this to a unique, unpredictable value, as detailed in the Django docs. One method of generating this key is provided in Adjusting the App Settings of the Scalable Django App tutorial.
      • DJANGO_ALLOWED_HOSTS: This variable secures the app and prevents HTTP Host header attacks. For testing purposes, set this to *, a wildcard that will match all hosts. In production you should set this to your_domain.com. To learn more about this Django setting, consult Core Settings from the Django docs.
      • DATABASE_USERNAME: Set this to the PostgreSQL database user created in the prerequisite steps.
      • DATABASE_PASSWORD: Set this to the PostgreSQL user password created in the prerequisite steps.
      • DATABASE_HOST: Set this to your database’s hostname.
      • DATABASE_PORT: Set this to your database’s port.
      • STATIC_ACCESS_KEY_ID: Set this to your S3 bucket or Space’s access key.
      • STATIC_SECRET_KEY: Set this to your S3 bucket or Space’s access key Secret.
      • STATIC_BUCKET_NAME: Set this to your S3 bucket or Space name.
      • STATIC_ENDPOINT_URL: Set this to the appropriate S3 bucket or Space endpoint URL, for example https://space-name.nyc3.digitaloceanspaces.com if your Space is located in the nyc3 region.

      Once you’ve finished editing, save and close the file.

      We’ll now use docker run to override the CMD set in the Dockerfile and create the database schema using the manage.py makemigrations and manage.py migrate commands:

      docker run --env-file env polls sh -c "python manage.py makemigrations && python manage.py migrate"
      

      We run the polls:latest container image, pass in the environment variable file we just modified, and override the Dockerfile command with sh -c "python manage.py makemigrations && python manage.py migrate", which will create the database schema defined by the app code. If you’re running this for the first time you should see:

      Output

      No changes detected Operations to perform: Apply all migrations: admin, auth, contenttypes, polls, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying polls.0001_initial... OK Applying sessions.0001_initial... OK

      This indicates that the database schema has successfully been created.

      If you’re running migrate a subsequent time, Django will perform a no-op unless the database schema has changed.

      Next, we’ll run another instance of the app container and use an interactive shell inside of it to create an administrative user for the Django project.

      docker run -i -t --env-file env polls sh
      

      This will provide you with a shell prompt inside of the running container which you can use to create the Django user:

      python manage.py createsuperuser
      

      Enter a username, email address, and password for your user, and after creating the user, hit CTRL+D to quit the container and kill it.

      Finally, we’ll generate the static files for the app and upload them to the DigitalOcean Space using collectstatic. Note that this may take a bit of time to complete.

      docker run --env-file env polls sh -c "python manage.py collectstatic --noinput"
      

      After these files are generated and uploaded, you’ll receive the following output.

      Output

      121 static files copied.

      We can now run the app:

      docker run --env-file env -p 80:8000 polls
      

      Output

      [2019-10-17 21:23:36 +0000] [1] [INFO] Starting gunicorn 19.9.0 [2019-10-17 21:23:36 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1) [2019-10-17 21:23:36 +0000] [1] [INFO] Using worker: sync [2019-10-17 21:23:36 +0000] [7] [INFO] Booting worker with pid: 7 [2019-10-17 21:23:36 +0000] [8] [INFO] Booting worker with pid: 8 [2019-10-17 21:23:36 +0000] [9] [INFO] Booting worker with pid: 9

      Here, we run the default command defined in the Dockerfile, gunicorn --bind :8000 --workers 3 mysite.wsgi:application, and expose container port 8000 so that port 80 on the Ubuntu server gets mapped to port 8000 of the polls container.

      You should now be able to navigate to the polls app using your web browser by typing http://APP_SERVER_1_IP in the URL bar. Since there is no route defined for the / path, you’ll likely receive a 404 Page Not Found error, which is expected.

      Warning: When using the UFW firewall with Docker, Docker bypasses any configured UFW firewall rules, as documented in this GitHub issue. This explains why you have access to port 80 of your server, even though you haven’t explicitly created a UFW access rule in any prerequisite step. In Step 5 we will address this security hole by patching the UFW configuration. If you are not using UFW and are using DigitalOcean’s Cloud Firewalls, you can safely ignore this warning.

      Navigate to http://APP_SERVER_1_IP/polls to see the Polls app interface:

      Polls Apps Interface

      To view the administrative interface, visit http://APP_SERVER_1_IP/admin. You should see the Polls app admin authentication window:

      Polls Admin Auth Page

      Enter the administrative username and password you created with the createsuperuser command.

      After authenticating, you can access the Polls app’s administrative interface:

      Polls Admin Main Interface

      Note that static assets for the admin and polls apps are being delivered directly from object storage. To confirm this, consult Testing Spaces Static File Delivery.

      When you are finished exploring, hit CTRL+C in the terminal window running the Docker container to kill the container.

      Now that you’ve confirmed that the app container runs as expected, you can run it in detached mode, which will run it in the background and allow you to log out of your SSH session:

      docker run -d --rm --name polls --env-file env -p 80:8000 polls
      

      The -d flag instructs Docker to run the container in detached mode, the -rm flag cleans up the container’s filesystem after the container exits, and we name the container polls.

      Log out of the first Django app server, and navigate to http://APP_SERVER_1_IP/polls to confirm that the container is running as expected.

      Now that your first Django app server is up and running, you can set up your second Django app server.

      Step 2 — Configuring the Second Django Application Server

      Since many of the commands to set up this server will be the same as those in the previous step, they will be presented here in abbreviated form. Please review Step 1 for more information on any particular command in this step.

      Begin by logging in to the second Django application server.

      Clone the polls-docker branch of the django-polls GitHub repository:

      • git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git

      Navigate into the django-polls directory:

      cd django-polls
      

      Build the image using docker build:

      Open the env file with nano or your favorite editor:

      nano env
      

      django-polls/env

      DJANGO_SECRET_KEY=
      DEBUG=True
      DJANGO_ALLOWED_HOSTS=
      DATABASE_ENGINE=postgresql_psycopg2
      DATABASE_NAME=polls
      DATABASE_USERNAME=
      DATABASE_PASSWORD=
      DATABASE_HOST=
      DATABASE_PORT=
      STATIC_ACCESS_KEY_ID=
      STATIC_SECRET_KEY=
      STATIC_BUCKET_NAME=
      STATIC_ENDPOINT_URL=
      DJANGO_LOGLEVEL=info
      

      Fill in the missing values as in Step 1. When you’ve finished editing, save and close the file.

      Finally, run the app container in detached mode:

      docker run -d --rm --name polls --env-file env -p 80:8000 polls
      

      Navigate to http://APP_SERVER_2_IP/polls to confirm that the container is running as expected. You can safely log out of the second app server without terminating your running container.

      With both Django app containers up and running, you can move on to configuring the Nginx reverse proxy container.

      Step 3 — Configuring the Nginx Docker Container

      Nginx is a versatile web server that offers a number of features including reverse proxying, load balancing, and caching. In this tutorial we’ve offloaded Django’s static assets to object storage, so we won’t use Nginx’s caching capabilities. However, we will use Nginx as a reverse proxy to our two backend Django app servers, and distribute incoming requests between them. In addition, Nginx will perform TLS termination and redirection using a TLS certificate provisioned by Certbot. This means that it will force clients to use HTTPS, redirecting incoming HTTP requests to port 443. It will then decrypt HTTPS requests and proxy them to the upstream Django servers.

      In this tutorial we’ve made the design decision to decouple the Nginx containers from the backend servers. Depending on your use case, you may choose to run the Nginx container on one of the Django app servers, proxying requests locally, as well as to the other Django server. Another possible architecture would be running two Nginx containers, one on each backend server, with a cloud load balancer in front. Each architecture presents different security and performance advantages, and you should load test your system to discover bottlenecks. The flexible architecture described in this tutorial allows you to scale both the backend Django app layer, as well as the Nginx proxying layer. Once the single Nginx container becomes a bottleneck, you can scale out to multiple Nginx proxies, and add a cloud load balancer or fast L4 load balancer like HAProxy.

      With both Django app servers up and running, we can begin setting up the Nginx proxy server. Log in to your proxy server and create a directory called conf:

      mkdir conf
      

      Create a configuration file called nginx.conf using nano or your favorite editor:

      nano conf/nginx.conf
      

      Paste in the following Nginx configuration:

      conf/nginx.conf

      
      upstream django {
          server APP_SERVER_1_IP;
          server APP_SERVER_2_IP;
      }
      
      server {
          listen 80 default_server;
          return 444;
      }
      
      server {
          listen 80;
          listen [::]:80;
          server_name your_domain.com;
          return 301 https://$server_name$request_uri;
      }
      
      server {
          listen 443 ssl http2;
          listen [::]:443 ssl http2;
          server_name your_domain.com;
      
          # SSL
          ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
          ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
      
          ssl_session_cache shared:le_nginx_SSL:10m;
          ssl_session_timeout 1440m;
          ssl_session_tickets off;
      
          ssl_protocols TLSv1.2 TLSv1.3;
          ssl_prefer_server_ciphers off;
      
          ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
      
          client_max_body_size 4G;
          keepalive_timeout 5;
      
              location / {
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
                proxy_set_header Host $http_host;
                proxy_redirect off;
                proxy_pass http://django;
              }
      
          location ^~ /.well-known/acme-challenge/ {
              root /var/www/html;
          }
      
      }
      

      These upstream, server, and location blocks configure Nginx to redirect HTTP requests to HTTPS, and load balance them across the two Django app servers configured in Steps 1 and 2. To learn more about Nginx configuration file structure, please refer to this article on Understanding the Nginx Configuration File Structure and Configuration Contexts. Additionally, this article on Understanding Nginx Server and Location Block Selection Algorithms may be helpful.

      This configuration was assembled from sample configuration files provided by Gunicorn, Cerbot, and Nginx and is meant as a minimal Nginx configuration to get this architecture up and running. Tuning this Nginx configuration goes beyond the scope of this article, but you can use a tool like NGINXConfig to generate performant and secure Nginx configuration files for your architecture.

      The upstream block defines the group of servers used to proxy requests to using the proxy_pass directive:

      conf/nginx.conf

      upstream django {
          server APP_SERVER_1_IP;
          server APP_SERVER_2_IP;
      }
      . . .
      

      In this block we name the upstream django and include the IP addresses of both Django app servers. If the app servers are running on DigitalOcean and have VPC Networking enabled, you should use their private IP addresses here. To learn how to enable VPC Networking on DigitalOcean, please see How to Enable VPC Networking on Existing Droplets.

      The first server block captures requests that do not match your domain and terminates the connection. For example, a direct HTTP request to your server’s IP address would be handled by this block:

      conf/nginx.conf

      . . .
      server {
          listen 80 default_server;
          return 444;
      }
      . . .
      

      The next server block redirects HTTP requests to your domain to HTTPS using an HTTP 301 redirect. These requests are then handled by the final server block:

      conf/nginx.conf

      . . .
      server {
          listen 80;
          listen [::]:80;
          server_name your_domain.com;
          return 301 https://$server_name$request_uri;
      }
      . . .
      

      These two directives define the paths to the TLS certificate and secret key. These will be provisioned using Certbot and mounted into the Nginx container in the next step.

      conf/nginx.conf

      . . .
      ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
      ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
      . . .
      

      These parameters are SSL security defaults recommended by Certbot. To learn more about them, please see Module ngx_http_ssl_module from the Nginx docs. Mozilla’s Security/Server Side TLS is another helpful guide that you can use to tune your SSL configuration.

      conf/nginx.conf

      . . .
          ssl_session_cache shared:le_nginx_SSL:10m;
          ssl_session_timeout 1440m;
          ssl_session_tickets off;
      
          ssl_protocols TLSv1.2 TLSv1.3;
          ssl_prefer_server_ciphers off;
      
          ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
      . . .
      

      These two directives from Gunicorn’s sample Nginx configuration set the maximum allowed size of the client request body and assign the timeout for keep-alive connections with the client. Nginx will close connections with the client after keepalive_timeout seconds.

      conf/nginx.conf

      . . .
      client_max_body_size 4G;
      keepalive_timeout 5;
      . . .
      

      The first location block instructs Nginx to proxy requests to the upstream django servers over HTTP. It additionally preserves client HTTP headers that capture the originating IP address, protocol used to connect, and target host:

      conf/nginx.conf

      . . .
      location / {
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header X-Forwarded-Proto $scheme;
          proxy_set_header Host $http_host;
          proxy_redirect off;
          proxy_pass http://django;
      }
      . . .
      

      To learn more about these directives, please see Deploying Gunicorn and Module ngx_http_proxy_module from the Nginx docs.

      The final location block captures requests to the /well-known/acme-challenge/ path, used by Certbot for HTTP-01 challenges to verify your domain with Let’s Encrypt and provision or renew TLS certificates. For more information on the HTTP-01 challenge used by Certbot, please see Challenge Types from the Let’s Encrypt docs.

      conf/nginx.conf

      . . .
      location ^~ /.well-known/acme-challenge/ {
              root /var/www/html;
      }
      

      Once you’ve finished editing, save and close the file.

      You can now use this configuration file to run an Nginx Docker container. In this tutorial we’ll use the nginx:1.19.0 image, version 1.19.0 of the official Docker image maintained by Nginx.

      When we run the container for the first time, Nginx will throw an error and fail as we haven’t yet provisioned the certificates defined in the configuration file. However, we’ll still run the command to download the Nginx image locally and test that everything else is functioning correctly:

      docker run --rm --name nginx -p 80:80 -p 443:443 
          -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro 
          -v /var/www/html:/var/www/html 
          nginx:1.19.0
      

      Here we name the container nginx and map the host ports 80 and 443 to the respective container ports. The -v flag mounts the config file into the Nginx container at /etc/nginx/conf.d/nginx.conf, which the Nginx image is preconfigured to load. It is mounted in ro or “read only” mode, so the container cannot modify the file. The web root directory /var/www/html is also mounted into the container. Finally nginx:1.19.0 instructs Docker to pull and run the nginx:1.19.0 image from Dockerhub.

      Docker will pull and run the image, then Nginx will throw an error when it doesn’t find the configured TLS certificate and secret key. In the next step we’ll provision these using a Dockerized Certbot client and the Let’s Encrypt certificate authority.

      Step 4 — Configuring Certbot and Let’s Encrypt Certificate Renewal

      Certbot is a Let’s Encrypt client developed by the Electronic Frontier Foundation. It provisions free TLS certificates from the Let’s Encrypt certificate authority which allow browsers to verify the identity of your web servers. Given that we have Docker installed on our Nginx proxy server, we’ll use the Certbot Docker image to provision and renew the TLS certificates.

      Begin by ensuring that you have a DNS A record mapped to the proxy server’s public IP address. Then, on your proxy server, provision a staging version of the certificates using the certbot Docker image:

      docker run -it --rm -p 80:80 --name certbot 
               -v "/etc/letsencrypt:/etc/letsencrypt" 
               -v "/var/lib/letsencrypt:/var/lib/letsencrypt" 
               certbot/certbot certonly --standalone --staging -d your_domain.com
      

      This command runs the certbot Docker image in interactive mode, and forwards port 80 on the host to container port 80. It creates and mounts two host directories into the container: /etc/letsencrypt/ and /var/lib/letsencrypt/. certbot is run in standalone mode, without Nginx, and will use the Let’s Encrypt staging servers to perform domain validation.

      When prompted, enter your email address and agree to the Terms of Service. If domain validation was successful, you should see the following output:

      Output

      Obtaining a new certificate Performing the following challenges: http-01 challenge for stubb.dev Waiting for verification... Cleaning up challenges IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/your_domain.com/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/your_domain.com/privkey.pem Your cert will expire on 2020-09-15. To obtain a new or tweaked version of this certificate in the future, simply run certbot again. To non-interactively renew *all* of your certificates, run "certbot renew" - Your account credentials have been saved in your Certbot configuration directory at /etc/letsencrypt. You should make a secure backup of this folder now. This configuration directory will also contain certificates and private keys obtained by Certbot so making regular backups of this folder is ideal.

      You can inspect the certificate using cat:

      sudo cat /etc/letsencrypt/live/your_domain.com/fullchain.pem
      

      With the TLS certificate provisioned, we can test the Nginx configuration assembled in the previous step:

      docker run --rm --name nginx -p 80:80 -p 443:443 
          -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro 
          -v /etc/letsencrypt:/etc/letsencrypt 
          -v /var/lib/letsencrypt:/var/lib/letsencrypt 
          -v /var/www/html:/var/www/html 
          nginx:1.19.0
      

      This is the same command run in Step 3, with the addition of both recently created Let’s Encrypt directories.

      Once Nginx is up and running, navigate to http://your_domain.com. You may receive a warning in your browser that the certificate authority is invalid. This is expected as we’ve provisioned staging certificates and not production Let’s Encrypt certificates. Check the URL bar of your browser to confirm that your HTTP request was redirected to HTTPS.

      Hit CTRL+C in your terminal to quit Nginx, and run the certbot client again, this time omitting the --staging flag:

      docker run -it --rm -p 80:80 --name certbot 
               -v "/etc/letsencrypt:/etc/letsencrypt" 
               -v "/var/lib/letsencrypt:/var/lib/letsencrypt" 
               certbot/certbot certonly --standalone -d your_domain.com
      

      When prompted to either keep the existing certificate or renew and replace it, hit 2 to renew it and then ENTER to confirm your choice.

      With the production TLS certificate provisioned, run the Nginx server once again:

      docker run --rm --name nginx -p 80:80 -p 443:443 
          -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro 
          -v /etc/letsencrypt:/etc/letsencrypt 
          -v /var/lib/letsencrypt:/var/lib/letsencrypt 
          -v /var/www/html:/var/www/html 
          nginx:1.19.0
      

      In your browser, navigate to http://your_domain.com. In the URL bar, confirm that the HTTP request has been redirected to HTTPS. Given that the Polls app has no default route configured, you should see a Django Page not found error. Navigate to https://your_domain.com/polls and you’ll see the standard Polls app interface:

      Polls Apps Interface

      At this point you’ve provisioned a production TLS certificate using the Certbot Docker client, and are reverse proxying and load balancing external requests to the two Django app servers.

      Let’s Encrypt certificates expire every 90 days. To ensure that your certificate remains valid, you should renew it regularly before its scheduled expiry. With Nginx running, you should use the Certbot client in webroot mode instead of standalone mode. This means that Certbot will perform validation by creating a file in the /var/www/html/.well-known/acme-challenge/ directory, and the Let’s Encrypt validation requests to this path will be captured by the location rule defined in the Nginx config in Step 3. Certbot will then rotate certificates, and you can reload Nginx so that it uses this newly provisioned certificate.

      There are multiple ways to automate this procedure and the automatic renewal of TLS certificates goes beyond the scope of this tutorial. For a similar process using the cron scheduling utility, please see Step 6 of How To Secure a Containerized Node.js Application with Nginx, Let’s Encrypt, and Docker Compose.

      In your terminal, hit CTRL+C to kill the Nginx container. Run it again in detached mode by appending the -d flag:

      docker run --rm --name nginx -d -p 80:80 -p 443:443 
          -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro 
          -v /etc/letsencrypt:/etc/letsencrypt 
          -v /var/lib/letsencrypt:/var/lib/letsencrypt 
        -v /var/www/html:/var/www/html 
          nginx:1.19.0
      

      With Nginx running in the background, use the following command to perform a dry run of the certificate renewal procedure:

      docker run -it --rm --name certbot 
          -v "/etc/letsencrypt:/etc/letsencrypt" 
        -v "/var/lib/letsencrypt:/var/lib/letsencrypt" 
        -v "/var/www/html:/var/www/html" 
        certbot/certbot renew --webroot -w /var/www/html --dry-run
      

      We use the --webroot plugin, specify the web root path, and use the --dry-run flag to verify that everything is working correctly without actually performing the certificate renewal.

      If the renewal simulation succeeds, you should see the following output:

      Output

      Cert not due for renewal, but simulating renewal for dry run Plugins selected: Authenticator webroot, Installer None Renewing an existing certificate Performing the following challenges: http-01 challenge for your_domain.com Using the webroot path /var/www/html for all unmatched domains. Waiting for verification... Cleaning up challenges - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - new certificate deployed without reload, fullchain is /etc/letsencrypt/live/your_domain.com/fullchain.pem - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates below have not been saved.) Congratulations, all renewals succeeded. The following certs have been renewed: /etc/letsencrypt/live/your_domain.com/fullchain.pem (success) ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates above have not been saved.) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      In a production setting, after renewing certificates, you should reload Nginx so that the changes take effect. To reload Nginx, run the following command:

      docker kill -s HUP nginx
      

      This command will send a HUP Unix signal to the Nginx process running inside of the nginx Docker container. Upon receiving this signal, Nginx will reload its configuration and renewed certificates.

      With HTTPS enabled and all the components of this architecture up and running, the final step is to lock down the setup by preventing external access to the two backend app servers; all HTTP requests should flow through the Nginx proxy.

      Step 5 — Preventing External Access to Django App Servers

      In the architecture described in this tutorial, SSL termination occurs at the Nginx proxy. This means that Nginx decrypts the SSL connection, and packets are proxied to the Django app servers unencrypted. For many use cases, this level of security is sufficient. For applications involving financial or health data, you may want to implement end-to-end encryption. You can do this by forwarding encrypted packets through the load balancer and decrypting on the app servers, or re encrypting at the proxy and once again decrypting on the Django app servers. These techniques go beyond the scope of this article, but to learn more please consult End-to-end encryption.

      The Nginx proxy acts as a gateway between external traffic and the internal network. Theoretically no external clients should have direct access to the internal app servers, and all requests should flow through the Nginx server. The note in Step 1 briefly describes an open issue with Docker where Docker bypasses ufw firewall settings by default and opens ports externally, which may be insecure. To address this security concern, it’s recommended to use cloud firewalls when working with Docker-enabled servers. To get more information on creating Cloud Firewalls with DigitalOcean, consult How to Create Firewalls. You can also manipulate iptables directly instead of using ufw. To learn more about using iptables with Docker, please see Docker and iptables.

      In this step we’ll modify UFW’s configuration to block external access to host ports opened by Docker. When running Django on the app servers, we passed the -p 80:8000 flag to docker, which forwards port 80 on the host to container port 8000. This also opened up port 80 to external clients, which you can verify by visiting http://your_app_server_1_IP. To prevent direct access, we’ll modify UFW’s configuration using the method described in the ufw-docker GitHub repository.

      Begin by logging in to the first Django app server. Then, open the /etc/ufw/after.rules file with superuser privileges, using nano or your favorite editor:

      sudo nano /etc/ufw/after.rules
      

      Enter your password when prompted, and hit ENTER to confirm.

      You should see the following ufw rules:

      /etc/ufw/after.rules

      #
      # rules.input-after
      #
      # Rules that should be run after the ufw command line added rules. Custom
      # rules should be added to one of these chains:
      #   ufw-after-input
      #   ufw-after-output
      #   ufw-after-forward
      #
      
      # Don't delete these required lines, otherwise there will be errors
      *filter
      :ufw-after-input - [0:0]
      :ufw-after-output - [0:0]
      :ufw-after-forward - [0:0]
      # End required lines
      
      # don't log noisy services by default
      -A ufw-after-input -p udp --dport 137 -j ufw-skip-to-policy-input
      -A ufw-after-input -p udp --dport 138 -j ufw-skip-to-policy-input
      -A ufw-after-input -p tcp --dport 139 -j ufw-skip-to-policy-input
      -A ufw-after-input -p tcp --dport 445 -j ufw-skip-to-policy-input
      -A ufw-after-input -p udp --dport 67 -j ufw-skip-to-policy-input
      -A ufw-after-input -p udp --dport 68 -j ufw-skip-to-policy-input
      
      # don't log noisy broadcast
      -A ufw-after-input -m addrtype --dst-type BROADCAST -j ufw-skip-to-policy-input
      
      # don't delete the 'COMMIT' line or these rules won't be processed
      COMMIT
      

      Scroll to the bottom, and paste in the following block of UFW configuration rules:

      /etc/ufw/after.rules

      . . .
      
      # BEGIN UFW AND DOCKER
      *filter
      :ufw-user-forward - [0:0]
      :DOCKER-USER - [0:0]
      -A DOCKER-USER -j RETURN -s 10.0.0.0/8
      -A DOCKER-USER -j RETURN -s 172.16.0.0/12
      -A DOCKER-USER -j RETURN -s 192.168.0.0/16
      
      -A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN
      
      -A DOCKER-USER -j ufw-user-forward
      
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12
      
      -A DOCKER-USER -j RETURN
      COMMIT
      # END UFW AND DOCKER
      

      These rules restrict public access to ports opened by Docker, and enable access from the 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16 private IP ranges. If you are using VPC with DigitalOcean, then Droplets in your VPC network will have access to the open port over the private network interface, but external clients will not. For more information about VPC, please see the VPC official documentation. To learn more about the rules implemented in this snippet, please see How it works? from the ufw-docker README.

      When you’ve finished editing, save and close the file.

      Restart ufw so that it picks up the new configuration:

      sudo systemctl restart ufw
      

      Navigate to http://APP_SERVER_1_IP in your web browser to confirm that you can no longer access the app server over port 80.

      Repeat this process on the second Django app server.

      Log out of the first app server or open another terminal window, and log in to the second Django app server. Then, open the /etc/ufw/after.rules file with superuser privileges, using nano or your favorite editor:

      sudo nano /etc/ufw/after.rules
      

      Enter your password when prompted, and hit ENTER to confirm.

      Scroll to the bottom, and paste in the following block of UFW configuration rules:

      /etc/ufw/after.rules

      . . .
      
      # BEGIN UFW AND DOCKER
      *filter
      :ufw-user-forward - [0:0]
      :DOCKER-USER - [0:0]
      -A DOCKER-USER -j RETURN -s 10.0.0.0/8
      -A DOCKER-USER -j RETURN -s 172.16.0.0/12
      -A DOCKER-USER -j RETURN -s 192.168.0.0/16
      
      -A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN
      
      -A DOCKER-USER -j ufw-user-forward
      
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12
      
      -A DOCKER-USER -j RETURN
      COMMIT
      # END UFW AND DOCKER
      

      When you’ve finished editing, save and close the file.

      Restart ufw so that it picks up the new configuration:

      sudo systemctl restart ufw
      

      Navigate to http://APP_SERVER_2_IP in your web browser to confirm that you can no longer access the app server over port 80.

      Finally, navigate to https://your_domain_here/polls to confirm that the Nginx proxy still has access to the upstream Django servers. You should see the default Polls app interface.

      Conclusion

      In this tutorial, you’ve set up a scalable Django Polls application using Docker containers. As your traffic grows and load on the system increases, you can scale each layer separately: the Nginx proxying layer, the Django backend app layer, and the PostgreSQL database layer.

      When building a distributed system, there are often multiple design decisions you must face, and several architectures may satisfy your use case. The architecture described in this tutorial is meant as a flexible blueprint for designing scalable apps with Django and Docker.

      You may wish to control the behavior of your containers when they encounter errors, or run containers automatically when your system boots. To do this, you can use a process manager like Systemd or implement restart policies. For more information about these, please see Start containers automatically from the Docker documentation.

      When working at scale with multiple hosts running the same Docker image, it can be more efficient to automate steps using a configuration management tool like Ansible or Chef. To learn more about configuration management, please consult An Introduction to Configuration Management and Automating Server Setup with Ansible: A DigitalOcean Workshop Kit.

      Instead of building the same image on every host, you can also streamline deployment using an image registry like Docker Hub, which centrally builds, stores, and distributes Docker images to multiple servers. Along with an image registry, a continuous integration and deployment pipeline can help you build, test, and deploy images to your app servers. For more information on CI/CD, please consult An Introduction to CI/CD Best Practices.



      Source link