Guia Definitivo de Docker em Produção: Do Dockerfile ao Compose Seguro
Já mostrei [como subir o stack completo desta aplicação em uma VPS](/blog/do-zero-a-producao-django-docker-vps/). Este artigo é o complemento: um checklist de **segurança e resiliência** para o `Dockerfile` e o `docker-compose.yml` — as decisões que separam um compose "que funciona" de um compose pronto para produção.
---
## 1. Usuário não-root no container
Um container rodando como root que sofre um RCE entrega ao atacante um processo root com acesso ao kernel do host. A mitigação custa três linhas:
```dockerfile
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser
```
A partir do `USER appuser`, qualquer exploração da aplicação fica confinada a um usuário sem privilégios.
## 2. Segredos fail-closed no Compose
A sintaxe de interpolação do Compose aceita dois modificadores com comportamentos opostos:
- `${VAR:-default}` — se `.env` não define `VAR`, usa o default **silenciosamente**.
- `${VAR:?erro}` — se `.env` não define `VAR`, o compose **se recusa a subir**.
Para segredos, só a segunda é aceitável:
```yaml
environment:
SECRET_KEY: ${SECRET_KEY:?Defina SECRET_KEY no .env}
DB_PASSWORD: ${DB_PASSWORD:?Defina DB_PASSWORD no .env}
ALLOWED_HOSTS: ${ALLOWED_HOSTS:?Defina ALLOWED_HOSTS no .env}
```
O pior incidente aqui não é o deploy que falha — é o deploy que sobe com `SECRET_KEY=insecure-default` e ninguém percebe por seis meses. **Fail-closed**: na dúvida, quebre cedo e com mensagem clara.
Note o `ALLOWED_HOSTS` na lista: um `${ALLOWED_HOSTS:-*}` de conveniência desfaria, num único deploy, o hardening de host validation configurado no Django.
## 3. `.dockerignore` é controle de segurança, não organização
O `COPY . /app/` do Dockerfile copia **tudo** que não estiver ignorado — inclusive o `.env` com credenciais reais. Camadas de imagem são inspecionáveis por qualquer pessoa que consiga fazer pull:
```
# .dockerignore
.env
.git
media/
venv/
```
Sem essa exclusão, suas credenciais de produção ficam gravadas permanentemente nas camadas da imagem, mesmo que você delete o arquivo numa camada posterior.
## 4. Healthchecks e ordem de subida
`depends_on` sem condição só garante ordem de *início*, não de *prontidão* — o MySQL demora vários segundos entre o processo iniciar e aceitar conexões:
```yaml
db:
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u root -p$${DB_ROOT_PASSWORD} || exit 1"]
interval: 5s
retries: 10
web:
depends_on:
db:
condition: service_healthy
```
Isso elimina a clássica corrida em que o Django tenta migrar antes do banco existir.
## 5. Rede interna: só o Nginx exposto
Num compose de produção, apenas o reverse proxy publica porta no host. Banco e Redis ficam acessíveis somente pela rede interna do Docker — nada de `3306:3306` "para facilitar o debug", que é como bancos acabam expostos na internet com senha fraca.
---
## Checklist final
1. `USER` não-root no Dockerfile.
2. Todo segredo com `${VAR:?erro}` — nunca `:-default`.
3. `.env` no `.dockerignore` (e no `.gitignore`).
4. Healthcheck no banco + `condition: service_healthy`.
5. Somente o proxy com portas publicadas.
Cada item custa minutos. A ausência de qualquer um deles já causou incidentes reais em produção mundo afora.
Comentários
Seja o primeiro a comentar.
Artigos relacionados
Como Proteger seu Servidor Linux contra Ataques Brute-Force e Port Scanning
Defesa em camadas para VPS Ubuntu: SSH só por chave, firewall deny-by-default, Fail2ban, superfície mínima de portas, patches automáticos e restrição por IPs do Cloudflare.
Engenharia de Prompt e Agentes de IA em Python: Como Construir Assistentes Especializados
Explorando a construção de agentes de IA desacoplados em Python com controle de estado, validação de saída estruturada e otimização de contexto para produção.
Como Construí uma Plataforma Educacional Gamificada em Django 6.0 (Build in Public)
Bastidores da arquitetura e desenvolvimento da plataforma de cursos gamificada em Django 6.0: Mimo UI, validação de quizzes AJAX, trava de lições sequencial, antispam e analytics no painel de monitoria.