Do Zero à Produção: Django + Docker em VPS
Colocar uma aplicação Django em produção não precisa ser um pesadelo de configurações manuais ou dependência de serviços caros de PaaS.
Neste tutorial prático, vou detalhar exatamente a arquitetura e o processo de deploy que utilizei para subir a **makisjeanty.com** em uma VPS rodando Ubuntu 24.04 com Docker, Nginx, Daphne (ASGI) e Cloudflare.
---
## 1. A Arquitetura do Stack
Para suportar tanto requisições HTTP tradicionais quanto WebSockets em tempo real (usados no chat do site), a topologia de containers ficou assim:
- **Nginx (Porta 80/443)**: Reverse proxy responsável por SSL termination, roteamento HTTP/WebSocket e arquivos estáticos via WhiteNoise.
- **Web (Daphne / ASGI)**: Servidor de aplicação Django rodando sob o processo Daphne na porta 8000.
- **DB (MySQL 8.0)**: Banco de dados relacional com volume persistente montado no host.
- **Redis 7**: Cache e Channel Layer para o Django Channels (WebSockets).
---
## 2. Dockerfile Otimizado para Produção
O `Dockerfile` utiliza um usuário não-root por segurança e instala dependências mínimas necessárias:
```dockerfile
FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends build-essential default-libmysqlclient-dev pkg-config && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Usuário não-root para execução segura
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
CMD ["daphne", "-b", "0.0.0.0", "-p", "8000", "core.asgi:application"]
```
---
## 3. Docker Compose com Fail-Closed para Segredos
Uma das decisões críticas no `docker-compose.yml` foi utilizar a sintaxe `${VAR:?mensagem}` para impedir que os containers subam caso alguma variável de ambiente crítica esteja ausente no `.env`:
```yaml
services:
web:
build: .
container_name: makis_web
restart: unless-stopped
environment:
- SECRET_KEY=${SECRET_KEY:?SECRET_KEY ausente}
- DB_PASSWORD=${DB_PASSWORD:?DB_PASSWORD ausente}
- ALLOWED_HOSTS=${ALLOWED_HOSTS:?ALLOWED_HOSTS ausente}
depends_on:
- db
- redis
db:
image: mysql:8.0
container_name: makis_mysql
restart: unless-stopped
environment:
- MYSQL_DATABASE=base_central
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD:?DB_ROOT_PASSWORD ausente}
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:7-alpine
container_name: makis_redis
restart: unless-stopped
volumes:
mysql_data:
```
---
## 4. Roteamento de WebSockets no Nginx
Para permitir conexões de WebSocket sem desconexões prematuras, o `nginx.conf` precisa repassar os cabeçalhos de `Upgrade` e `Connection` para o Daphne:
```nginx
server {
listen 80;
server_name makisjeanty.com;
location / {
proxy_pass http://web:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Rota específica para WebSockets
location /ws/ {
proxy_pass http://web:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
}
```
---
## 5. Health Check Embutido (`/health/`)
Em vez de depender apenas da porta responder, criei um endpoint de monitoria em `core/urls.py` que testa a conexão com o banco e o Redis em tempo real:
```python
def health_check(request):
status = {"status": "ok", "database": "ok", "redis": "ok"}
http_code = 200
try:
connection.ensure_connection()
except Exception:
status["database"] = "error"
status["status"] = "degraded"
http_code = 503
return JsonResponse(status, status=http_code)
```
---
## 6. Resultado e Métricas
Com essa estrutura, a aplicação responde com tempo médio abaixo de 100ms para requisições HTTP, possui suporte total a real-time chat via WebSockets e roda de forma estável com baixo consumo de recursos na VPS.
Comentários
Seja o primeiro a comentar.
Artigos relacionados
A Anatomia de um Timing Attack e Como Mitigar com HMAC em Python
Por que comparar tokens com == vaza segredos por canal lateral de tempo, e como hmac.compare_digest e HMAC-SHA256 resolvem — com o caso real do webhook de pagamento deste site.
Como Estruturei uma Arquitetura Web Resiliente com Django 6, Docker e Cloudflare em VPS
Guia prático e estudo de caso real sobre como provisionar um servidor de produção resiliente com SSL Full, containerização não-root, cache em Redis e monitoramento de saúde.
RUNBOOK que Funciona às 3h da Manhã: Arquitetura de Resposta a Incidentes
Detalhes da arquitetura de observabilidade e resposta a incidentes em Django 6: separação de liveness vs readiness probes, segurança em webhooks e painel administrativo.