SSD Nodes Learn 🎉 VPS desde $5.50/mês
Guias Matt ConnorPor Matt Connor

Django ou Flask numa VPS pequena: qual consome menos?

Compare Django e Flask numa VPS de 1 a 2 GB: veja a memória residente por worker do gunicorn e quantos workers cabem sem sobrecarregar o servidor.

Custo do Django e do Flask numa VPS pequena

Django versus Flask numa VPS pequena é, em primeiro lugar, uma questão de memória. O Django carrega o seu object relational mapper (ORM), o sistema de migrações e, se o activar, o site de administração em todos os processos worker que iniciar. O Flask carrega um router e um objecto de pedido. Numa máquina com 1 GB, essa diferença determina quantos workers cabem, e o número de workers determina quantos pedidos pode servir em simultâneo.

Esse custo só pesa contra o Django se nunca reconstruir aquilo que ele fornece. Uma aplicação com contas de utilizador, sessões e um painel de administração beneficia do Django: a RAM por worker é o preço do código que não precisa de escrever. Uma API JSON à frente de um datastore que já executa beneficia do Flask, porque nenhuma dessas funcionalidades seria carregada. É uma questão de adequação. As medições abaixo indicam de que lado a sua aplicação se encontra.

Quanta memória usa um worker do gunicorn?

ChartMemory per gunicorn worker, minimal app, three workers with preload on
The data behind this chart
[
  {
    "label": "Bare Python 3.12 process",
    "rss_mb": 14,
    "pss_mb": 9
  },
  {
    "label": "Flask, one route",
    "rss_mb": 42,
    "pss_mb": 26
  },
  {
    "label": "Flask + SQLAlchemy",
    "rss_mb": 58,
    "pss_mb": 38
  },
  {
    "label": "Django, admin disabled",
    "rss_mb": 78,
    "pss_mb": 47
  },
  {
    "label": "Django, admin enabled",
    "rss_mb": 96,
    "pss_mb": 58
  }
]

Estes são valores publicados típicos para uma aplicação hello world de cada tipo no Ubuntu 24.04 com Python 3.12, três workers do gunicorn e preload ativado. Considere-os um valor mínimo, porque os seus próprios imports são adicionados a estes valores. Um worker do Django com o admin ativado apresenta 96 MB residentes, enquanto a sua parcela proporcional de memória é de 58 MB. A diferença entre estes dois valores é o tema da próxima secção.

Faça a mesma medição no seu próprio sistema.

sudo apt update && sudo apt install -y python3-venv
python3 -m venv /srv/site1/.venv
/srv/site1/.venv/bin/pip install django gunicorn setproctitle

Instale setproctitle. Com ele instalado, o gunicorn renomeia os seus processos para gunicorn: master [site1] e gunicorn: worker [site1]. É isso que permite aos comandos seguintes encontrar os workers pelo nome, em vez de depender de estimativas.

pgrep -af gunicorn
ps -o pid,rss,args -p $(pgrep -d, -f 'gunicorn: worker')

A coluna rss é o resident set size em kilobytes: cada página de memória que o processo mantém atualmente na RAM. Somá-la entre os workers produz um valor demasiado alto, porque um worker criado com fork partilha páginas com o processo pai e com os processos irmãos. Assim, a mesma página é contabilizada várias vezes. Em vez disso, peça ao kernel o proportional set size (PSS), que divide cada página partilhada entre os processos que a mapeiam.

for pid in $(pgrep -f 'gunicorn: worker'); do awk -v p="$pid" '/^Pss:/ {printf "%s  %d MB\n", p, $2/1024}' /proc/$pid/smaps_rollup; done

Execute o comando como o utilizador proprietário dos workers ou com sudo. O PSS é a coluna que deve usar para calcular o orçamento de memória, porque o PSS é somado corretamente e o RSS não.

O Django é maior por causa do que django.setup() faz. Importa cada entrada em INSTALLED_APPS, cria o registo de aplicações e instancia cada classe de modelo, juntamente com um objeto Python para cada campo. Adicionar django.contrib.admin executa a autodiscovery do admin, que importa o módulo admin de cada aplicação e carrega também as camadas de formulários e templates. Um worker do Flask importa Werkzeug e Jinja2 e termina.

Uma ressalva importante: o framework é muitas vezes a parte menor. Um worker que importa um SDK de cloud ou qualquer biblioteca numérica carrega mais memória por causa disso do que por causa do Django. Meça a sua aplicação real antes de concluir que o problema é o framework.

Copy on write e por que o preload altera o número

O processo master do Gunicorn cria os workers com fork. Imediatamente depois de fork(), o processo filho partilha todas as páginas de memória com o processo pai, e o kernel só copia uma página quando um dos lados escreve nela. Assim, o facto de o registo de modelos do Django existir uma vez ou quatro vezes no servidor depende do lado do fork em que foi criado.

Com preload_app desativado, cada worker importa a aplicação depois de o fork ter sido feito, por isso cada um cria a sua própria cópia privada. Com essa opção ativada, o master importa a aplicação uma vez e os workers herdam essas páginas.

import gc

bind = "unix:/run/site1/gunicorn.sock"
umask = 0o007
workers = 3
timeout = 30
preload_app = True
max_requests = 500
max_requests_jitter = 50

def when_ready(server):
    gc.freeze()

O CPython não favorece o copy on write. O cabeçalho de cada objeto contém uma contagem de referências, e tocar num objeto escreve nesse cabeçalho. Por isso, as páginas partilhadas acabam copiadas uma a uma à medida que o garbage collector percorre o heap. gc.freeze() move tudo o que foi alocado até esse momento para uma geração permanente que o collector deixa de visitar, mantendo mais dessas páginas partilhadas. when_ready é o hook correto porque é executado depois do preload e antes de o primeiro worker ser criado com fork. Meça o PSS antes e depois de o adicionar, porque a poupança depende da quantidade de estado da aplicação criado durante a importação.

O preload tem um custo que surpreende algumas pessoas no dia do deploy. systemctl reload envia HUP, e o comportamento documentado do gunicorn em HUP consiste em recarregar a configuração e iniciar novos workers. Quando a aplicação é preloaded, o código não é importado novamente, por isso a nova versão não está a ser executada, embora os processos worker sejam novos. Use systemctl restart depois de uma alteração ao código, ou a sequência USR2 e depois WINCH se precisar de deixar os workers antigos terminar primeiro.

Quantos workers um VPS de 1 GB consegue executar realisticamente?

ChartWhere a 1 GB VPS goes, typical idle figures before any traffic
The data behind this chart
[
  {
    "label": "Ubuntu 24.04 base",
    "ram_mb": 190
  },
  {
    "label": "nginx",
    "ram_mb": 12
  },
  {
    "label": "PostgreSQL, default config",
    "ram_mb": 120
  },
  {
    "label": "Headroom you must leave",
    "ram_mb": 150
  },
  {
    "label": "Left for gunicorn workers",
    "ram_mb": 550
  }
]

Esses são valores em idle num servidor que não serve nada. Restam cerca de 550 MB para os workers da aplicação, antes de chegar o primeiro pedido.

Agora faça as contas com uma margem pessimista. Um pedido consome memória enquanto está a ser processado: uma queryset que carrega alguns milhares de linhas, seguida da renderização de um template. O pico por worker costuma aproximar-se do dobro do valor em idle, por isso faça o orçamento com o dobro. Django com o admin, a 58 MB em idle, permite quatro workers neste servidor. Flask com SQLAlchemy, a 38 MB, permite sete.

A sugestão (2 x cores) + 1 do Gunicorn assume que a CPU é o recurso escasso e que a RAM não é. Num VPS pequeno, o cenário é o inverso. Um vCPU partilhado também fornece menos capacidade do que um núcleo completo quando o host está ocupado. Deve compreender isto antes de culpar o seu código: o tempo de steal da CPU causado por um vizinho ruidoso aparece em top como o valor st.

Se as suas views passam a maior parte do tempo à espera de uma base de dados ou de uma API upstream, use threads em vez de processos. --worker-class gthread --workers 2 --threads 4 permite oito pedidos concorrentes pelo custo de memória de dois workers, porque os threads partilham uma única cópia carregada do interpretador e do framework. O global interpreter lock significa que os threads não ajudam numa view que consome CPU.

Configure swap no servidor. Um VPS de 1 GB sem swap transforma um pico de memória num processo terminado, enquanto um swapfile transforma o mesmo pico num pedido lento.

sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness = 10' | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl --system

Depois, limite a própria aplicação. MemoryMax=600M na unidade do gunicorn faz com que o kernel recupere a memória do cgroup da sua aplicação, em vez de escolher uma vítima em todo o servidor. Assim, um pedido descontrolado não lhe encerra a sessão SSH.

Comportamento do arranque a frio e dos reinícios

ChartImport to ready, minimal app, one shared vCPU
The data behind this chart
[
  {
    "label": "Flask, one route",
    "cold_start_ms": 90
  },
  {
    "label": "Flask + SQLAlchemy",
    "cold_start_ms": 260
  },
  {
    "label": "Django, admin disabled",
    "cold_start_ms": 480
  },
  {
    "label": "Django, admin enabled",
    "cold_start_ms": 720
  }
]

O custo de arranque é pago duas vezes: em cada deploy e em cada reinício automático após uma falha. Uma aplicação Flask mínima fica pronta em cerca de 90 ms, e o Django com o administrador ativado demora cerca de 720 ms na mesma vCPU partilhada. Ambos são valores publicados típicos. Meça o seu próprio ambiente, porque as dependências têm o maior impacto.

cd /srv/site1
DJANGO_SETTINGS_MODULE=site1.settings /srv/site1/.venv/bin/python -X importtime -c "import django; django.setup()" 2>&1 | tail -20

As últimas linhas listam as importações mais lentas, com os microssegundos cumulativos. Para Flask, execute a mesma flag no seu módulo: python -X importtime -c "import app".

Com o preload ativado, o processo master paga esse custo uma vez e cada worker criado com fork arranca imediatamente. Com o preload desativado, cada worker paga esse custo, e o timeout do gunicorn abrange tanto o arranque como o pedido. Um worker que não tenha comunicado dentro de timeout segundos é terminado e substituído. Assim, uma aplicação pesada numa vCPU partilhada lenta pode ficar num ciclo de reinícios sem nunca servir um pedido. O log apresenta:

[2026-08-09 09:14:02 +0000] [981] [CRITICAL] WORKER TIMEOUT (pid:1004)

As migrações devem estar na unit, não no código de arranque da aplicação. ExecStartPre é executado uma vez antes de existir qualquer worker. Colocar migrate dentro da aplicação faz com que três workers concorram pelo mesmo bloqueio do schema.

A estrutura de implementação é quase igual

Gestor de processos

Ambos os frameworks são executados com gunicorn, e o gunicorn é executado pelo systemd.

[Unit]
Description=gunicorn for site1
After=network.target

[Service]
User=site1
Group=www-data
WorkingDirectory=/srv/site1
RuntimeDirectory=site1
Environment="PATH=/srv/site1/.venv/bin"
ExecStartPre=/srv/site1/.venv/bin/python manage.py migrate --noinput
ExecStart=/srv/site1/.venv/bin/gunicorn -c /srv/site1/gunicorn.conf.py site1.wsgi:application
Restart=on-failure
RestartSec=3
MemoryMax=600M

[Install]
WantedBy=multi-user.target

RuntimeDirectory=site1 cria /run/site1 ao iniciar e remove-o ao parar, por isso o caminho do socket existe sempre com o proprietário correto. A linha umask = 0o007 na configuração do gunicorn é o que torna esse socket gravável pelo grupo www-data, permitindo que o nginx lhe aceda.

A unidade do Flask é o mesmo ficheiro com uma linha alterada: ExecStart=... gunicorn -c /srv/site1/gunicorn.conf.py app:app, e sem ExecStartPre. O argumento app:app é o módulo seguido do callable, por isso o erro Failed to find attribute 'app' in 'app'. significa que o módulo não define uma variável com esse nome. As tarefas agendadas seguem o mesmo padrão, e um temporizador do systemd substitui o cron para um comando de gestão do Django sem adicionar uma fila de tarefas a um servidor deste tamanho.

Ficheiros estáticos

O Django com DEBUG = False não serve ficheiros estáticos. Defina STATIC_ROOT, execute python manage.py collectstatic e aponte o servidor Web para o diretório de saída. Se ignorar esse passo, o painel de administração carrega sem estilos e o log fica cheio de Not Found: /static/admin/css/base.css.

Há duas formas razoáveis de os servir. Um bloco alias do nginx não consome recursos da aplicação. O WhiteNoise, adicionado como middleware, serve os ficheiros a partir do worker e elimina a necessidade do bloco no nginx, mas consome algum tempo do worker por ficheiro. O Flask serve a sua própria pasta static/ em desenvolvimento; em produção, aponte o proxy para essa pasta pelo mesmo motivo.

Reverse proxy

server {
    listen 80;
    server_name example.com;

    location /static/ {
        alias /srv/site1/static/;
        expires 30d;
    }

    location / {
        proxy_pass http://unix:/run/site1/gunicorn.sock;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Atrás de qualquer proxy, é necessário informar o Django de que o pedido original usou HTTPS. Caso contrário, as verificações de falsificação de pedidos entre sites (CSRF) rejeitam os seus próprios formulários.

ALLOWED_HOSTS = ["example.com"]
CSRF_TRUSTED_ORIGINS = ["https://example.com"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

Se o servidor já executar contentores, o Traefik à frente de várias aplicações Docker Compose faz o mesmo trabalho usando labels no contentor, em vez de um ficheiro por site.

Qual base de dados

O SQLite é realmente adequado para um único servidor de aplicações com uma taxa de escrita de algumas operações por segundo, além de eliminar um daemon do orçamento de memória. Ative o registo write ahead (WAL) e defina um timeout de espera no driver.

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
        "OPTIONS": {
            "timeout": 20,
            "init_command": "PRAGMA journal_mode=WAL;",
        },
    }
}

Sem estas duas opções, encontra django.db.utils.OperationalError: database is locked logo na primeira vez que dois workers escrevem em simultâneo, porque o modo de journal predefinido bloqueia os leitores durante uma escrita e o timeout predefinido desiste quase imediatamente. A explicação mais detalhada, incluindo o ponto em que o SQLite deixa de ser a escolha adequada, está em executar SQLite em produção numa VPS.

O PostgreSQL no mesmo servidor com 1 GB custa os 120 MB indicados no orçamento acima, além de um processo backend para cada ligação persistente. O CONN_MAX_AGE do Django mantém uma ligação aberta por worker, por isso quatro workers significam quatro backends. Normalmente, é uma troca adequada. Conte esse consumo antes de definir o número de workers. Se preferir manter a base de dados num contentor ao lado da aplicação, executar Docker numa VPS representa a mesma troca com um limite mínimo mais elevado, porque o daemon e cada contentor acrescentam overhead relevante neste tamanho.

O que as baterias oferecem e quanto custam

Os megabytes adicionais do Django correspondem a funcionalidades que já existem e já funcionam em conjunto: o ORM com migrações, o sistema de sessões e autenticação, o modelo de permissões, a camada de formulários com proteção CSRF, o motor de templates, os comandos de gestão e o admin. O admin é a funcionalidade que muitas pessoas subestimam. É um editor de base de dados funcional para os seus modelos, com pesquisa e filtros, por uma linha em INSTALLED_APPS.

O Flask é o oposto. Obtém encaminhamento, um objeto de pedido, templates Jinja2 e um objeto de configuração. Tudo o resto é uma escolha sua. Isso representa uma vantagem real quando a aplicação é pequena, porque nenhum ORM é carregado se nunca importar um.

A armadilha está no meio-termo. Adicione SQLAlchemy para os modelos, Alembic para as migrações, Flask-Login para as sessões, Flask-WTF para os formulários e a proteção CSRF e uma extensão de admin para o back office, e terá montado algo com o perfil de memória do Django e sem a sua coerência. Cada componente tem o seu próprio ciclo de lançamentos e a sua própria opinião sobre como a aplicação deve ser ligada. É nesse ponto que o Django passa a ser a opção mais económica, tanto em RAM como nas horas gastas em atualizações.

Django vs Flask: regra de decisão

Use Django quando a aplicação tiver contas de utilizador, conteúdo editável, um esquema que continuará a mudar e um back office que alguém irá realmente abrir. Use Flask quando a aplicação for uma interface JSON sobre um datastore já existente ou um recetor de webhook sem HTML.

O critério de desempate é uma lista escrita. Anote todos os pacotes que instalaria no Flask para obter o conjunto de funcionalidades de que precisa. Se essa lista incluir um ORM e uma ferramenta de migração, já escolheu Django e está a pagar mais para chegar ao mesmo resultado lentamente.

Há um caso que favorece genuinamente o Flask em hardware limitado: vários serviços pequenos no mesmo servidor. Cada serviço Flask é um processo económico independente, executado na sua própria unidade. Três sites Django num VPS com 1 GB significam três cópias do framework residentes em simultâneo, e o cálculo acima deixa de funcionar. Se o resultado continuar a ser insuficiente, escolher um plano maior é muitas vezes a solução correta, e quanto custa realmente um VPS por mês é uma conversa mais curta do que reescrever uma aplicação funcional.

Falhas e mensagens que verá

Os workers desaparecem e voltam. O Gunicorn apresenta [ERROR] Worker (pid:1234) was sent SIGKILL! Perhaps out of memory?. Isto ocorre tanto quando termina um worker que não enviou o heartbeat dentro do timeout como quando o kernel terminou o processo. Distinga os dois casos com dmesg -T | grep -i "killed process". Uma linha nesse resultado indica falta de memória; reduza o número de workers ou adicione swap.

Todas as páginas devolvem 400 e o log indica Invalid HTTP_HOST header. A mensagem completa é Invalid HTTP_HOST header: 'example.com'. You may need to add 'example.com' to ALLOWED_HOSTS.. O Django rejeita o pedido antes de este chegar ao seu código porque ALLOWED_HOSTS está vazio ou não inclui o nome que o proxy passou em Host.

Os formulários falham com Origin checking failed. A página indica que a verificação CSRF falhou. Isto acontece atrás de um proxy com terminação TLS: a aplicação vê HTTP simples, cria uma origem http:// e compara-a com um pedido que chegou por https://. Defina SECURE_PROXY_SSL_HEADER e CSRF_TRUSTED_ORIGINS e confirme que o proxy envia efetivamente X-Forwarded-Proto.

O nginx devolve 502 imediatamente. O log de erros indica o motivo: connect() to unix:/run/site1/gunicorn.sock failed (2: No such file or directory) significa que a unidade não está em execução, e (13: Permission denied) significa que o socket existe, mas o nginx não consegue abri-lo. Isto corresponde à configuração de umask e do grupo.

O painel de administração não tem estilos. collectstatic não foi executado ou o caminho alias não corresponde a STATIC_ROOT. O access log mostra respostas 404 em /static/admin/.

As escritas falham com pouca carga. database is locked do SQLite indica que o WAL está desativado ou que o timeout de espera está demasiado curto para dois workers escreverem ao mesmo tempo.

FAQ

O Django é demasiado pesado para um VPS com 1 GB?

Não. O Django, com um número reduzido de workers, nginx à frente e SQLite por trás, funciona sem problemas com 1 GB. A margem fica reduzida quando adiciona PostgreSQL com as definições predefinidas, uma cache, um worker em segundo plano e Docker no mesmo servidor. Meça o proportional set size de um worker, duplique esse valor para acomodar picos de pedidos e compare o total com a memória que sobra depois do sistema operativo e da base de dados.

Quantos workers do gunicorn devo executar num vCPU?

Comece com três e faça medições. Num VPS pequeno, a memória é normalmente a principal limitação. Divida a RAM que sobra depois do sistema operativo e da base de dados pelo dobro do proportional set size de um worker. Se as suas views passarem a maior parte do tempo à espera de uma base de dados ou de uma API upstream, mude para a classe de workers gthread, com um número reduzido de workers e várias threads em cada um. As threads partilham uma cópia carregada do framework e consomem muito menos memória do que processos adicionais.

Preciso de PostgreSQL ou o SQLite é suficiente?

O SQLite é suficiente para um servidor de aplicações com uma taxa de escrita moderada e elimina um daemon completo do orçamento de memória. Ative o write ahead logging e defina um busy timeout. Caso contrário, as escritas concorrentes falham com database is locked. Mude para PostgreSQL quando mais de uma máquina tiver de escrever ou quando precisar de algo que o SQLite não oferece, como suporte para vários writers pesados em simultâneo ou controlo de acesso por role.

Devo executar uvicorn em vez de gunicorn?

Apenas se tiver views assíncronas e algo real pelo qual esperar. O Flask é uma aplicação WSGI. Por isso, uma view assíncrona é executada num novo event loop dentro da thread do worker e termina antes de começar o pedido seguinte. Isto não proporciona concorrência adicional. As views assíncronas do Django precisam de um servidor ASGI para obter qualquer benefício. As versões recentes do uvicorn moveram a classe de workers do gunicorn para um pacote separado. Consulte a documentação atual do uvicorn em vez de copiar uma flag de classe de workers antiga de um tutorial desatualizado.

Por que motivo o meu worker desapareceu sem traceback?

Um processo terminado pelo out of memory killer do kernel recebe SIGKILL e não consegue registar nada antes de sair. Por isso, o log da aplicação simplesmente para. O Gunicorn deteta a interrupção e apresenta Worker (pid:1234) was sent SIGKILL! Perhaps out of memory?. Confirme-a com dmesg -T | grep -i "killed process". A solução é reduzir o número de workers ou criar um swapfile, para que um pico de utilização de memória resulte num pedido lento em vez de num processo terminado.

#django#flask#python#gunicorn#deployment#vps