Como publicar Django na VPS com Gunicorn, Docker e HTTPS

Ilustração de servidor VPS com containers Docker, Gunicorn e cadeado de HTTPS representando deploy de aplicação Django

Fala aí, beleza? Se tu quer colocar um projeto Django no ar numa VPS do jeito certo, a resposta curta é esta: o Gunicorn roda a aplicação como servidor WSGI (comando base gunicorn myproject.wsgi), o Nginx fica na frente como proxy reverso cuidando do HTTPS e dos arquivos estáticos, e tudo isso vai empacotado em containers Docker

E nada de runserver em produção, combinado? Desde o Django 5.2 o próprio comando avisa que é um servidor de desenvolvimento e que não deve ser usado em produção 😛

O cenário aqui é um projeto em Django 6.1, a versão estável atual (lançada em 5 de agosto de 2026), com o Gunicorn na versão mais recente disponível (a 26.2.2, de 6 de setembro de 2026), cuja página oficial de instalação pede Python 3.12 ou superior

A espinha do deploy é sempre a mesma: DEBUG desligado, ALLOWED_HOSTS certinho, SECRET_KEY fora do git, estáticos com collectstatic, migrate aplicado e proxy confiável

Ainda está no Django 6.0? Se liga: ele saiu do suporte principal com a chegada do 6.1, o último bugfix foi o 6.0.8 (que também foi uma versão de segurança), e agora ele só recebe correções de segurança e de perda de dados até abril de 2027…

O que você precisa antes de publicar o Django na VPS

Antes de abrir o terminal, confere se o básico está no lugar, porque é aqui que muito deploy trava:

  • Python 3.12 ou superior, que é o que a página oficial de instalação do Gunicorn exige hoje (e a versão também precisa ser suportada pelo Django que tu usa)
  • Projeto Django com manage.py e o módulo wsgi (o myproject/wsgi.py que o próprio Django gera)
  • Migrations já criadas no desenvolvimento e versionadas no git, junto com o código
  • Domínio apontado para o IP da VPS, porque o HTTPS e o ALLOWED_HOSTS dependem dele
  • Uma VPS com acesso root, já que tu vai instalar e controlar Docker e Nginx por conta própria

Pra esse projeto, a recomendação principal da Hora de Codar é a VPS da Hostinger (este artigo contém links de afiliado da Hostinger)

O motivo é bem prático: a VPS vem com acesso root completo, o que é obrigatório pra rodar Docker e Nginx do seu jeito, e existe um template com Ubuntu 24.04 que já traz docker-ce e docker-compose instalados, aí tu pula a parte chata da instalação

Também tem terminal SSH direto no navegador com acesso root, armazenamento SSD NVMe, processadores AMD EPYC, proteção contra DDoS e opção de servidor no Brasil, o que ajuda quando o seu público está por aqui 🙂

E qual plano pegar? A linha KVM vai do KVM 1 (1 vCPU, 4 GB de RAM, 50 GB NVMe) até o KVM 8 (8 vCPU, 32 GB de RAM, 400 GB NVMe)

O número de vCPUs conversa direto com o Gunicorn: a orientação dele usa como ponto de partida (2 x núcleos) + 1 workers, então no KVM 1 isso dá 3 workers e no KVM 8 dá 17

Passo 1: ajuste os settings do Django para produção

O primeiro passo é fazer o Django ler as configurações sensíveis do ambiente, com DEBUG desligado e os hosts declarados

Por que tanto cuidado com o DEBUG? Porque ligado em produção ele expõe trechos do código-fonte, variáveis locais, settings e as bibliotecas que tu usa, tudo de bandeja pra quem provocar um erro na sua página

O checklist de deploy do Django é claro: DEBUG nunca ligado em produção, e a SECRET_KEY de produção deve ser exclusiva do ambiente e ficar fora do controle de versão (variável de ambiente é uma das formas indicadas)

Um exemplo didático do trecho de settings:

import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

DEBUG = False

# Vem do ambiente, nunca do git
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']

# Só o domínio, SEM esquema
ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '').split(',')

# COM esquema (exigência desde o Django 4.0)
CSRF_TRUSTED_ORIGINS = os.environ.get('DJANGO_CSRF_TRUSTED_ORIGINS', '').split(',')

STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'

E as variáveis ficam num .env que só existe no servidor, nunca no repositório:

DJANGO_SECRET_KEY=troque-por-uma-chave-unica-de-producao
DJANGO_ALLOWED_HOSTS=exemplo.com.br,www.exemplo.com.br
DJANGO_CSRF_TRUSTED_ORIGINS=https://exemplo.com.br,https://www.exemplo.com.br

E o que é esse STATIC_ROOT? É a pasta pra onde o collectstatic vai copiar todos os estáticos do projeto, e é ela que o Nginx vai servir no caminho do STATIC_URL lá na frente

Resultado esperado: o projeto carrega com os settings de produção, e se a SECRET_KEY não estiver no ambiente o Python já reclama logo de cara, em vez de subir com uma chave errada

Erro comum deste passo: colocar https:// no ALLOWED_HOSTS ou esquecer o esquema no CSRF_TRUSTED_ORIGINS. Um leva só o domínio, o outro leva o domínio COM https://, não inverte!

E tome cuidado com curinga no ALLOWED_HOSTS: ao usar, a validação do cabeçalho Host passa a ser responsabilidade sua

Passo 2: rode o Django com Gunicorn dentro do container

Aqui o Gunicorn substitui o runserver como quem de fato atende as requisições

Que servidor é esse? O Gunicorn é um servidor WSGI para UNIX escrito em Python puro, sem dependências, e se você conhece o runserver, pensa nele como a versão feita pra aguentar produção

Instala via pip e coloca no requirements.txt:

python -m pip install gunicorn

O comando base aponta pro módulo WSGI e roda na mesma pasta do manage.py:

gunicorn myproject.wsgi

Só que tem um detalhe: por padrão ele sobe um processo com uma thread, escutando em 127.0.0.1:8000

No seu PC isso funciona, mas dentro do container o 127.0.0.1 é o próprio container, aí o Nginx rodando em outro container não alcança o Gunicorn

Por isso, no exemplo abaixo ele escuta numa interface alcançável pela rede do Compose e já sobe com 3 workers, que é o (2 x 1) + 1 de uma VPS com 1 vCPU como o KVM 1

Um Dockerfile mínimo, como exemplo didático (não testado pelo autor):

# Exemplo didático, não testado pelo autor
# Informe uma versão de Python suportada pelo seu Django e pelo Gunicorn (3.12 ou superior)
ARG PYTHON_VERSION
FROM python:${PYTHON_VERSION}

WORKDIR /app

COPY requirements.txt .
RUN python -m pip install -r requirements.txt

COPY . .

# Roda na pasta do manage.py e escuta fora do 127.0.0.1
CMD ["gunicorn", "myproject.wsgi", "--bind", "0.0.0.0:8000", "--workers", "3"]

Se tu já montou a API NestJS com Docker Compose, a lógica é bem parecida: a aplicação fica num container e o proxy em outro

Erro comum deste passo: manter o bind padrão em 127.0.0.1 e ficar horas tentando entender por que o Nginx não acha o Gunicorn (já vi muita gente perder a tarde com isso haha)

Passo 3: coloque o Nginx na frente e configure o proxy confiável

A documentação de deploy do Gunicorn recomenda fortemente rodar o Gunicorn atrás de um proxy, e o Nginx é a opção mais indicada

O Nginx faz três trabalhos aqui: termina o HTTPS, serve a pasta STATIC_ROOT no caminho STATIC_URL e valida o Host, recusando requisições pra hosts incorretos antes delas chegarem ao Django

Sobre o HTTPS: o certificado do seu domínio precisa estar configurado no Nginx, porque é ele quem termina o TLS e conversa com o Gunicorn por dentro da rede dos containers

Agora o ponto que mais derruba deploy com Docker, se liga nisso:

Por padrão, o Gunicorn só confia nos cabeçalhos X-Forwarded-* quando a conexão vem do localhost (127.0.0.1, o valor padrão de forwarded_allow_ips)

Com o Nginx em OUTRO container, ele não é localhost, então o Gunicorn ignora esses cabeçalhos e o Django nunca fica sabendo que a requisição original veio por HTTPS

A solução é liberar o IP do proxy com --forwarded-allow-ips, informando o IP que o container do Nginx usa na rede do Compose

E o '*'? Só é seguro se o Gunicorn estiver totalmente isolado da rede externa, porque com conexões não confiáveis qualquer um pode forjar esses cabeçalhos

Um docker-compose de exemplo com os dois serviços (exemplo didático, não testado pelo autor):

# Exemplo didático, não testado pelo autor
services:
  web:
    build:
      context: .
      args:
        PYTHON_VERSION: "SUA_VERSAO_DO_PYTHON"
    env_file: .env
    command: >
      gunicorn myproject.wsgi
      --bind 0.0.0.0:8000
      --workers 3
      --forwarded-allow-ips=IP_DO_NGINX
    volumes:
      - static:/app/staticfiles
    # sem "ports": o Gunicorn não fica exposto para fora

  nginx:
    image: nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - static:/srv/static:ro
      # aqui entram a sua configuração do Nginx e o certificado do domínio
    depends_on:
      - web

volumes:
  static:

O volume static é o truque pros estáticos: o Django escreve nele via collectstatic e o Nginx lê a mesma pasta e serve no /static/

A mesma ideia de aplicação + proxy aparece no deploy do Astro com SSR, caso tu queira comparar as duas montagens

Do lado do Django, entra o SECURE_PROXY_SSL_HEADER:

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

Só que essa linha tem regra, e é das sérias: use apenas se as TRÊS condições forem verdadeiras

  • o Django está atrás de um proxy
  • o proxy remove o X-Forwarded-Proto de todas as requisições que recebe
  • o proxy só envia esse cabeçalho em requisições que chegaram por HTTPS

Erro comum deste passo: ativar o SECURE_PROXY_SSL_HEADER sem o Nginx limpar o X-Forwarded-Proto recebido. Aí um atacante manda o cabeçalho na mão e faz qualquer requisição parecer segura, ou seja, tu desliga a proteção achando que ligou 😬

Passo 4: suba os containers na VPS e aplique migrations e estáticos

Com o projeto pronto, agora é hora de levar tudo pra VPS e aplicar o que o banco e o Nginx precisam

Na Hostinger com o template Docker, o fluxo fica assim:

  1. Use o template com Ubuntu 24.04, que já vem com docker-ce e docker-compose instalados
  2. Acesse a VPS como root com ssh root@IP_DA_VPS, ou pelo terminal direto no navegador (a Web Console da Hostinger já abre a sessão logada como root), sem instalar nada no seu PC
  3. Confira a instalação com docker --version e docker-compose --version; se os dois responderem com a versão, o ambiente está pronto
  4. No painel da VPS, abra o Docker Manager, oferecido sem custo nas VPS com o template Docker, e implante o seu Compose por URL de repositório (GitHub, GitLab ou link direto) ou colando o YAML manualmente
  5. Garanta que as variáveis do .env cheguem ao container sem passar pelo git, porque a SECRET_KEY não pode ir pro repositório de jeito nenhum
  6. Com os containers no ar, rode no container da aplicação os dois comandos abaixo
python manage.py migrate
python manage.py collectstatic

O migrate aplica no banco de produção as migrations que vieram versionadas junto com o código, e o collectstatic copia todos os estáticos pra STATIC_ROOT, que é a pasta que o Nginx serve

O Docker Manager também serve pra acompanhar os containers depois, o que ajuda bastante quando algo cai e tu precisa saber qual deles foi

Erro comum deste passo: criar migrations em produção em vez de trazê-las do desenvolvimento. Migration nasce no ambiente de dev, vai pro git com o código e em produção só roda migrate, beleza?

Passo 5: valide o deploy com check --deploy

A checagem final é o comando que o próprio Django oferece pra caçar problema de segurança e configuração:

python manage.py check --deploy

Rodado contra os settings de produção, ele aponta avisos como security.W004 (SECURE_HSTS_SECONDS não definido), security.W012 (SESSION_COOKIE_SECURE), security.W016 (CSRF_COOKIE_SECURE) e security.W018 (DEBUG ligado)

Resolve o que ele apontar e roda de novo, até não sobrar alerta crítico

Depois confere no navegador:

  • o site abre pelo domínio em HTTPS
  • CSS, JS e imagens carregam pelo Nginx no /static/
  • um formulário com CSRF (o login do admin já serve) envia sem erro
  • os logs do container não mostram o aviso de servidor de desenvolvimento

Erro comum deste passo: rodar o check --deploy com os settings de desenvolvimento e achar que está tudo certo. Tem que rodar DENTRO do container de produção, com o mesmo .env que o site usa

Erros comuns ao publicar Django com Gunicorn e Nginx

Quase todo problema de deploy de Django cai em configuração, não em falta de máquina, então antes de pensar em trocar de plano, passa por esta lista

Bad Request ao abrir o site

  • Causa: ALLOWED_HOSTS ausente ou errado com DEBUG = False, e nesse cenário o Django não funciona sem hosts adequados
  • Correção: coloque o domínio (e o www, se usar) no ALLOWED_HOSTS, sem https://
  • Prevenção: mantenha os hosts em variável de ambiente e confira o valor antes de cada deploy

Formulário falhando por CSRF

  • Causa: CSRF_TRUSTED_ORIGINS sem o esquema, o que não vale desde o Django 4.0
  • Correção: use o formato completo, como https://exemplo.com.br
  • Prevenção: lembre da regra: ALLOWED_HOSTS sem esquema, CSRF_TRUSTED_ORIGINS com esquema

CSS e JS sem carregar

  • Causa: o collectstatic não rodou, ou o Nginx não está servindo a pasta STATIC_ROOT no STATIC_URL
  • Correção: rode python manage.py collectstatic e confirme que o volume dos estáticos está montado no Nginx
  • Prevenção: inclua o collectstatic na rotina de todo deploy

Django sem reconhecer o HTTPS atrás do proxy

  • Causa: o Gunicorn ignora os X-Forwarded-* porque o Nginx está em outro container e só conexões do localhost são confiáveis por padrão
  • Correção: libere o IP do proxy com --forwarded-allow-ips e só use o SECURE_PROXY_SSL_HEADER se as três condições do Passo 3 forem verdadeiras
  • Prevenção: nada de '*' se o Gunicorn tiver qualquer exposição à rede externa

Aviso de servidor de desenvolvimento nos logs

  • Causa: o container ainda sobe com runserver, que não passou por auditorias de segurança nem por testes de desempenho
  • Correção: troque o comando do container pelo Gunicorn apontando pro myproject.wsgi
  • Prevenção: confira o command do Compose e o CMD do Dockerfile antes de publicar

Vídeo de apoio do canal

Neste vídeo o Matheus apresenta o Hermes Desktop, o app que dá interface ao Hermes Agent e permite rodar o agente de IA local ou numa VPS 😀

Vídeo complementar do canal Hora de Codar, sobre um assunto relacionado.

Próximo passo: proteja e mantenha o Django em produção

Com o site no ar, a prioridade vira não perder nada quando algo der errado

Na Hostinger, todos os planos de VPS têm backup semanal gratuito, então avalia se o seu banco muda rápido demais pra depender só dele

Antes de atualizar o Django ou as imagens dos containers, tira um snapshot manual

Só toma cuidado: cada VPS guarda um único snapshot por vez, e ele é apagado se você reinstalar o sistema ou restaurar um backup, então não trata o snapshot como backup de longo prazo!

Se o projeto ainda está no Django 6.0, já coloca no calendário a migração pro 6.1, porque o 6.0 só recebe correções de segurança e de perda de dados até abril de 2027

E toda vez que mexer em settings, roda o check --deploy de novo, é rapidinho e evita surpresa em produção

Se o próximo passo é escolher onde rodar, dá pra conferir a VPS para este projeto e começar pelo plano que bate com a quantidade de workers que tu precisa

Até o próximo post! 🙂

Perguntas frequentes

Precisa rodar o Gunicorn dentro de Docker ou dá pra instalar direto na VPS?

O comando gunicorn myproject.wsgi funciona tanto dentro de um container quanto instalado direto na VPS, a lógica de bind e de workers é a mesma nos dois casos. A vantagem de usar Docker é isolar o ambiente da aplicação e padronizar o deploy junto com o Nginx, que é como este post monta o cenário.

O comando check --deploy do Django substitui a revisão do checklist de deploy?

Não totalmente. O check --deploy cobre itens como SECURE_HSTS_SECONDS (W004), SESSION_COOKIE_SECURE (W012), CSRF_COOKIE_SECURE (W016) e DEBUG (W018), mas o checklist oficial de deploy do Django inclui outros pontos que o comando não verifica, como ter STATIC_ROOT configurado e rodar o collectstatic. Use os dois juntos.


Quer aprender mais sobre inteligência artificial e programação na prática? Conheça o canal Hora de Codar no YouTube e acompanhe os próximos vídeos.

Escrito por | Matheus Battisti

Matheus Battisti
Fundador da Hora de Codar

Programador apaixonado pelo mundo das tecnologias, sempre buscando em aprender e se aprofundar em linguagens, frameworks e o que mais for necessário para executar um bom trabalho. Agora tem uma nova missão que é de passar seu conhecimento adiante para formar novos programadores e especializar mais os que já são.

Formações

Formação Claude Code

Formação Claude Code

Domine Claude Code do absoluto zero até o avançado

  • 120 aulas
  • 4 projetos
  • 9h 48min

Blog | Mais populares