Colocar uma aplicação Django em Docker para produção significa transformar o projeto em um conjunto de serviços previsíveis, isolados e reproduzíveis. Em vez de depender de configurações manuais em cada servidor, a aplicação passa a ser empacotada em uma imagem e executada em containers, com as mesmas versões de dependências e o mesmo comportamento em diferentes ambientes.
Um empacotamento bem-feito não é apenas “rodar o Django no Docker”. Envolve criar imagens pequenas, evitar segredos dentro do build, rodar como usuário não privilegiado, separar configurações de desenvolvimento e produção, e organizar serviços como banco de dados, cache, fila e proxy reverso. O resultado é um ambiente mais seguro, mais simples de escalar e mais fácil de operar.
Por que Docker faz diferença em projetos Django
O Docker resolve problemas clássicos de consistência e repetibilidade, pois a aplicação passa a executar no mesmo ambiente em qualquer lugar. O termo isolamento significa que dependências e versões ficam “presas” dentro do container, reduzindo conflitos entre projetos. A reprodutibilidade aparece quando um mesmo artefato (a imagem) pode ser construído e executado de forma idêntica em homologação e produção. A escalabilidade fica mais clara quando múltiplos containers podem ser criados para atender mais tráfego, com um balanceador na frente.
Mesmo assim, o Docker não corrige problemas de arquitetura, falta de testes ou código frágil. Um container apenas empacota o que já existe e torna o processo mais previsível. Quando a aplicação tem configurações misturadas, segredos hardcoded e dependências sem pinagem, o Docker só “congela” esses problemas. Por isso, a containerização para produção precisa tratar imagem, configuração, rede e execução com cuidado.
Estrutura recomendada de projeto para containerização
Uma estrutura clara ajuda a separar responsabilidades e reduz o risco de acoplamento entre ambientes. Em projetos Django, costuma funcionar bem dividir settings (configurações) por ambiente e separar scripts operacionais em uma pasta própria. Também é importante manter arquivos de infraestrutura como Dockerfile, docker-compose e configuração do Nginx organizados, para evitar que “virem parte do código” sem controle.
O exemplo abaixo mostra uma estrutura típica que atende bem desenvolvimento e produção. Ela permite builds previsíveis, facilita o uso de variáveis de ambiente e deixa explícito o que é “app” e o que é “infra”. Essa separação reduz a chance de um ajuste de produção quebrar o ambiente local, e vice-versa.
- config/ (settings base, development e production; urls; wsgi/asgi)
- apps/ (apps Django, como core, users, billing)
- requirements/ (base.txt, development.txt, production.txt)
- scripts/ (entrypoint.sh e scripts operacionais)
- nginx/ (nginx.conf, conf.d/default.conf)
- Dockerfile, docker-compose.yml, docker-compose.prod.yml
- .dockerignore, .env.example
Dockerfile profissional com Multi-Stage Build
O Multi-Stage Build é uma técnica do Docker para criar imagens menores e mais seguras, separando etapas de compilação e execução. A “stage” de build instala dependências que exigem compilação e gera um ambiente pronto, e a “stage” final leva apenas o necessário para rodar. Isso reduz tamanho, remove ferramentas desnecessárias e diminui superfície de ataque.
Outra prática essencial é rodar o processo como usuário não-root, evitando privilégios altos no container. Também ajuda isolar pacotes Python em um venv (ambiente virtual), que pode ser copiado entre estágios. Por fim, um HEALTHCHECK oferece um sinal objetivo para orquestradores e automações, indicando se a aplicação está saudável.
# Dockerfile
# ============================================
# Stage 1: Build stage
# ============================================
FROM python:3.12-slim AS builder
# Variáveis de ambiente para comportamento consistente do Python/Pip
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Dependências de build (compilação de wheels, drivers, etc.)
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Ambiente virtual isolado
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Instala dependências Python (produção)
COPY requirements/base.txt requirements/production.txt /tmp/requirements/
RUN pip install --upgrade pip && \
pip install -r /tmp/requirements/production.txt
# ============================================
# Stage 2: Production stage
# ============================================
FROM python:3.12-slim AS production
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONFAULTHANDLER=1 \
PATH="/opt/venv/bin:$PATH" \
APP_HOME=/app
# Dependências apenas de runtime (sem build-essential)
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
curl \
netcat-openbsd \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# Usuário não-root
RUN groupadd --gid 1000 appgroup && \
useradd --uid 1000 --gid appgroup --shell /bin/bash --create-home appuser
WORKDIR $APP_HOME
# Copia o venv pronto do builder
COPY --from=builder /opt/venv /opt/venv
# Copia código da aplicação
COPY --chown=appuser:appgroup . .
# Script de entrada (migrations/collectstatic e checagens)
COPY --chown=appuser:appgroup scripts/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Pastas para estáticos e mídia (quando servidos via volume)
RUN mkdir -p staticfiles media && \
chown -R appuser:appgroup staticfiles media
USER appuser
EXPOSE 8000
# Health check (exige endpoint /health/)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health/ || exit 1
ENTRYPOINT ["/entrypoint.sh"]
# Gunicorn: servidor WSGI recomendado para produção
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4"]
# ============================================
# Stage 3: Development stage
# ============================================
FROM production AS development
USER root
# Dependências de desenvolvimento (linters, pytest, debug toolbar, etc.)
COPY requirements/development.txt /tmp/requirements/
RUN pip install -r /tmp/requirements/development.txt
USER appuser
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
Entrypoint: migrações, estáticos e espera do banco
O entrypoint é um script executado antes do comando principal do container. Ele é útil para tarefas que precisam ocorrer sempre que um container sobe, como aguardar o banco ficar disponível, rodar migrations e executar collectstatic. O termo migração significa aplicar mudanças de modelo no banco usando arquivos gerados pelo Django.
Um cuidado importante é não deixar o entrypoint “mágico” demais, pois isso pode esconder problemas operacionais. Por isso, flags como AUTO_MIGRATE e AUTO_COLLECTSTATIC ajudam a controlar comportamento por ambiente. Outra boa prática é usar um comando final com exec, garantindo que o processo principal receba sinais corretamente (por exemplo, para desligamento limpo em deploys).
#!/bin/bash
# scripts/entrypoint.sh
set -e
echo "Iniciando entrypoint..."
# Espera banco ficar pronto (DATABASE_URL ou DB_HOST/DB_PORT)
if [ -n "$DATABASE_URL" ] || [ -n "$DB_HOST" ]; then
echo "Aguardando banco de dados..."
if [ -n "$DATABASE_URL" ]; then
# Exemplo: postgres://usuario:senha@host:porta/nome
DB_HOST=$(echo "$DATABASE_URL" | sed -e 's/.*@\([^:]*\).*/\1/')
DB_PORT=$(echo "$DATABASE_URL" | sed -e 's/.*:\([0-9]*\)\/.*/\1/')
fi
DB_HOST=${DB_HOST:-localhost}
DB_PORT=${DB_PORT:-5432}
while ! nc -z "$DB_HOST" "$DB_PORT"; do
echo "Banco ainda não está pronto..."
sleep 1
done
echo "Banco pronto!"
fi
# Migrações opcionais
if [ "$AUTO_MIGRATE" = "true" ]; then
echo "Aplicando migrations..."
python manage.py migrate --noinput
fi
# Coleta de arquivos estáticos opcional
if [ "$AUTO_COLLECTSTATIC" = "true" ]; then
echo "Executando collectstatic..."
python manage.py collectstatic --noinput
fi
echo "Iniciando processo principal..."
exec "$@"
.dockerignore: builds mais rápidos e sem vazamento de segredos
O arquivo .dockerignore define o que não deve ser enviado como “contexto” para o build. Isso acelera builds e evita incluir arquivos sensíveis dentro das camadas da imagem. Um erro comum é permitir que arquivos .env e bancos locais sejam copiados para a imagem, criando risco real de vazamento de credenciais.
Também é comum enviar pastas de IDE, caches e testes sem necessidade, aumentando a imagem e o tempo de build. Ao ignorar arquivos locais e artefatos de desenvolvimento, o build fica mais determinístico. Além disso, impede que arquivos grandes como mídia e estáticos sejam enviados sem controle, o que é especialmente relevante quando existe pipeline de CI.
# .dockerignore
# Git
.git
.gitignore
# Docker
Dockerfile*
docker-compose*
.docker
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
.venv/
venv/
ENV/
.eggs/
*.egg-info/
*.egg
# Testes
.pytest_cache/
.coverage
htmlcov/
.tox/
# IDE
.idea/
.vscode/
*.swp
*.swo
# Ambiente
.env
.env.*
!.env.example
# Arquivos locais
*.log
*.sqlite3
db.sqlite3
media/
staticfiles/
# Documentação
docs/
*.md
!README.md
# Diversos
.DS_Store
Thumbs.db
*.bak
Docker Compose no desenvolvimento: paridade com produção
O Docker Compose define múltiplos serviços e redes de forma declarativa, como aplicação web, banco e cache. A ideia de “paridade” é reduzir diferenças entre desenvolvimento e produção, evitando que algo funcione localmente e falhe no deploy. No desenvolvimento, volumes normalmente montam o código no container para refletir mudanças sem rebuild.
É comum incluir PostgreSQL como banco e Redis como cache e broker de filas. Quando há tarefas assíncronas, o Celery costuma rodar em serviços separados, com um worker e um scheduler (beat). Healthchecks no Compose ajudam a ordenar inicialização com dependências realmente prontas.
version: "3.8"
services:
db:
image: postgres:16-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD: devpassword
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
web:
build:
context: .
target: development
volumes:
- .:/app
- static_volume:/app/staticfiles
- media_volume:/app/media
ports:
- "8000:8000"
environment:
DJANGO_SETTINGS_MODULE: config.settings.development
DATABASE_URL: postgres://myapp:devpassword@db:5432/myapp
REDIS_URL: redis://redis:6379/0
DEBUG: "True"
SECRET_KEY: dev-secret-key-nao-usar-em-producao
AUTO_MIGRATE: "true"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
celery:
build:
context: .
target: development
command: celery -A config worker -l INFO
volumes:
- .:/app
environment:
DJANGO_SETTINGS_MODULE: config.settings.development
DATABASE_URL: postgres://myapp:devpassword@db:5432/myapp
REDIS_URL: redis://redis:6379/0
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
celery-beat:
build:
context: .
target: development
command: celery -A config beat -l INFO
volumes:
- .:/app
environment:
DJANGO_SETTINGS_MODULE: config.settings.development
DATABASE_URL: postgres://myapp:devpassword@db:5432/myapp
REDIS_URL: redis://redis:6379/0
depends_on:
- celery
volumes:
postgres_data:
redis_data:
static_volume:
media_volume:
Docker Compose em produção: serviços, redes e limites
Em produção, o Compose tende a trocar “conveniência” por segurança e previsibilidade. Variáveis sensíveis deixam de ser hardcoded e passam a ser fornecidas por variáveis de ambiente ou arquivos protegidos, como .env.production. É comum adicionar Nginx como proxy reverso, responsável por TLS, cache e entrega de arquivos estáticos.
Duas redes ajudam a separar tráfego interno e externo, reduzindo exposição de serviços como banco e Redis. O uso de restart policy melhora resiliência e resource limits impõem limites de CPU e memória. O Redis em produção costuma usar senha e persistência, pois pode atuar como broker e cache crítico.
version: "3.8"
services:
db:
image: postgres:16-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
redis:
image: redis:7-alpine
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
web:
build:
context: .
target: production
environment:
DJANGO_SETTINGS_MODULE: config.settings.production
DATABASE_URL: postgres://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
SECRET_KEY: ${SECRET_KEY}
ALLOWED_HOSTS: ${ALLOWED_HOSTS}
AUTO_MIGRATE: "true"
AUTO_COLLECTSTATIC: "true"
volumes:
- static_volume:/app/staticfiles
- media_volume:/app/media
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- backend
- frontend
deploy:
resources:
limits:
cpus: "1"
memory: 1G
celery:
build:
context: .
target: production
command: celery -A config worker -l WARNING --concurrency=4
environment:
DJANGO_SETTINGS_MODULE: config.settings.production
DATABASE_URL: postgres://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
SECRET_KEY: ${SECRET_KEY}
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- backend
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
celery-beat:
build:
context: .
target: production
command: celery -A config beat -l WARNING
environment:
DJANGO_SETTINGS_MODULE: config.settings.production
DATABASE_URL: postgres://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
SECRET_KEY: ${SECRET_KEY}
restart: unless-stopped
depends_on:
- celery
networks:
- backend
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- static_volume:/app/staticfiles:ro
- media_volume:/app/media:ro
- ./certbot/conf:/etc/letsencrypt:ro
- ./certbot/www:/var/www/certbot:ro
depends_on:
- web
restart: unless-stopped
networks:
- frontend
volumes:
postgres_data:
redis_data:
static_volume:
media_volume:
networks:
frontend:
backend:
Arquivo de ambiente de produção e cuidados com segredos
Um arquivo como .env.production organiza variáveis sensíveis e parâmetros de ambiente. O termo SECRET_KEY é uma chave usada pelo Django para criptografia e assinaturas, e não deve ter valor padrão em produção. O ALLOWED_HOSTS controla quais domínios podem acessar a aplicação, reduzindo riscos de host header attacks.
Em produção, segredos não devem ser commitados nem “baked” na imagem, pois camadas do Docker podem ser inspecionadas. Variáveis de ambiente e arquivos externos montados em runtime reduzem esse risco, embora ainda exijam controle de acesso. A prática mais segura é tratar segredos como material de infraestrutura, com permissões estritas.
# .env.production (nunca versionar)
SECRET_KEY=chave-super-secreta-de-producao
DEBUG=False
ALLOWED_HOSTS=seudominio.com,www.seudominio.com
DB_NAME=myapp_production
DB_USER=myapp_prod
DB_PASSWORD=senha-forte-do-banco
REDIS_PASSWORD=senha-forte-do-redis
Nginx na frente: TLS, estáticos e proxy reverso
O Nginx atua como proxy reverso, recebendo requisições externas e encaminhando para o serviço web interno. Isso permite terminar TLS (HTTPS), aplicar headers de segurança e servir arquivos estáticos com cache eficiente. O termo upstream define um grupo de servidores internos, e o Nginx encaminha requisições para eles.
Em produção, é comum redirecionar HTTP para HTTPS e tratar desafios de certificado, como o caminho /.well-known/acme-challenge/. Arquivos em /static/ e /media/ são entregues diretamente pelo Nginx, evitando passar pelo Django. Essa separação reduz carga na aplicação e melhora latência.
# nginx/conf.d/default.conf
upstream django {
server web:8000;
}
server {
listen 80;
server_name seudominio.com www.seudominio.com;
location / {
return 301 https://$host$request_uri;
}
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
}
server {
listen 443 ssl http2;
server_name seudominio.com www.seudominio.com;
ssl_certificate /etc/letsencrypt/live/seudominio.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/seudominio.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
location /static/ {
alias /app/staticfiles/;
expires 30d;
add_header Cache-Control "public, immutable";
}
location /media/ {
alias /app/media/;
expires 7d;
add_header Cache-Control "public";
}
location / {
proxy_pass http://django;
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;
proxy_redirect off;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
location /health/ {
proxy_pass http://django;
proxy_set_header Host $host;
access_log off;
}
}
Endpoint de Health Check no Django
O health check é um endpoint simples que indica se a aplicação está operacional. Ele costuma validar dependências críticas, como banco e cache, para evitar que o container seja considerado saudável quando não consegue atender requisições reais. O termo status code representa o código HTTP retornado, e 503 sinaliza indisponibilidade.
Ao consultar o banco com um SELECT 1, a aplicação valida conectividade e credenciais. Ao testar o cache, confirma que operações básicas funcionam, o que é relevante quando Redis é usado por sessões, cache ou Celery. Essa abordagem é simples e eficaz, desde que não execute consultas pesadas.
# apps/core/views.py
from django.http import JsonResponse
from django.db import connection
from django.core.cache import cache
def health_check(request):
"""Endpoint de verificação de saúde para orquestração e monitoramento."""
health = {
"status": "healthy",
"database": "ok",
"cache": "ok",
}
try:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
except Exception as erro:
health["status"] = "unhealthy"
health["database"] = str(erro)
try:
cache.set("health_check", "ok", 1)
if cache.get("health_check") != "ok":
raise Exception("Leitura do cache falhou")
except Exception as erro:
health["status"] = "unhealthy"
health["cache"] = str(erro)
status_code = 200 if health["status"] == "healthy" else 503
return JsonResponse(health, status=status_code)
# config/urls.py
from django.urls import path
from apps.core.views import health_check
urlpatterns = [
path("health/", health_check, name="health_check"),
]
Makefile: padronização de comandos operacionais
O Makefile centraliza comandos repetidos em alvos curtos, reduzindo erros de digitação e padronizando rotinas. Isso é útil quando há múltiplos arquivos Compose, como desenvolvimento e produção. Também simplifica a operação diária de logs, migrações e testes, sem memorizar parâmetros longos.
Esse arquivo não é obrigatório, mas ajuda a manter consistência em times e ambientes. O termo .PHONY marca alvos que não representam arquivos reais, garantindo execução correta. A seção de “Utilities” costuma incluir limpeza de volumes e imagens, com cuidado para não apagar dados importantes de produção.
# Makefile
.PHONY: build up down logs shell bash migrate makemigrations test createsuperuser \
prod-build prod-up prod-down prod-logs clean clean-all
# Desenvolvimento
build:
docker-compose build
up:
docker-compose up -d
down:
docker-compose down
logs:
docker-compose logs -f
shell:
docker-compose exec web python manage.py shell
bash:
docker-compose exec web bash
migrate:
docker-compose exec web python manage.py migrate
makemigrations:
docker-compose exec web python manage.py makemigrations
test:
docker-compose exec web pytest
createsuperuser:
docker-compose exec web python manage.py createsuperuser
# Produção
prod-build:
docker-compose -f docker-compose.prod.yml build
prod-up:
docker-compose -f docker-compose.prod.yml up -d
prod-down:
docker-compose -f docker-compose.prod.yml down
prod-logs:
docker-compose -f docker-compose.prod.yml logs -f
# Utilitários
clean:
docker-compose down -v --remove-orphans
docker system prune -f
clean-all:
docker-compose down -v --remove-orphans
docker system prune -af --volumes
Variáveis de ambiente no Django: development vs production
Variáveis de ambiente são chaves e valores fornecidos em runtime para configurar a aplicação sem alterar código. Em desenvolvimento, é comum ter defaults para acelerar o uso local, como uma SECRET_KEY simples. Em produção, defaults para segredos são perigosos, então valores críticos precisam ser obrigatórios.
Uma biblioteca comum para ler variáveis é a python-decouple, que oferece uma função config para buscar valores e converter tipos. O termo cast significa “converter” um valor de string para outro formato, como lista. Para ALLOWED_HOSTS, uma string com vírgulas vira uma lista de domínios.
# config/settings/development.py
from decouple import config
DEBUG = True
SECRET_KEY = config("SECRET_KEY", default="dev-secret-key")
# config/settings/production.py
from decouple import config
DEBUG = False
SECRET_KEY = config("SECRET_KEY") # sem default: obrigatório em produção
ALLOWED_HOSTS = config(
"ALLOWED_HOSTS",
cast=lambda valor: [s.strip() for s in valor.split(",")],
)
DATABASE_URL e parsing: um padrão simples para conectar
O DATABASE_URL é um formato de string que descreve a conexão com o banco, como usuário, senha, host, porta e nome do banco. Ele reduz quantidade de variáveis e facilita integração com plataformas e orquestradores. A biblioteca dj-database-url interpreta essa string e gera a configuração do Django automaticamente.
Manter compatibilidade com variáveis separadas (DB_NAME, DB_USER etc.) ajuda em cenários onde DATABASE_URL não é fornecida. O termo conn_max_age define por quanto tempo uma conexão pode ser reutilizada, melhorando performance. Essa abordagem cria um caminho consistente para desenvolvimento e produção sem duplicar lógica.
# config/settings/base.py
from decouple import config
import dj_database_url
DATABASE_URL = config("DATABASE_URL", default=None)
if DATABASE_URL:
DATABASES = {
"default": dj_database_url.parse(DATABASE_URL, conn_max_age=60)
}
else:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": config("DB_NAME"),
"USER": config("DB_USER"),
"PASSWORD": config("DB_PASSWORD"),
"HOST": config("DB_HOST", default="localhost"),
"PORT": config("DB_PORT", default="5432"),
}
}
Segurança em Docker: imagem, build e runtime
Segurança em container não é um único recurso, e sim um conjunto de escolhas pequenas. Na imagem, versões pinadas e base slim reduzem dependências e vulnerabilidades. No build, multi-stage remove ferramentas de compilação da imagem final e o .dockerignore evita enviar segredos e arquivos desnecessários.
No runtime, rodar como usuário não-root é uma das medidas mais efetivas. Segmentar redes separa serviços internos do tráfego externo e reduz exposição acidental. Limites de CPU e memória evitam que um processo descontrolado comprometa o host e outros serviços.
Os pontos abaixo resumem práticas importantes que costumam cobrir a maioria dos cenários comuns. Eles não eliminam a necessidade de monitoramento e atualização, mas reduzem riscos estruturais. O objetivo é evitar imagens gigantes, segredos em camadas e permissões exageradas.
- Imagens oficiais e versões fixas em vez de “latest”.
- Container rodando como usuário não-root.
- Multi-Stage Build para não levar ferramentas de build para produção.
- Segredos apenas por variáveis de ambiente ou mecanismos equivalentes, nunca no repositório.
- Healthchecks para sinalizar estado real do serviço.
- Redes separadas (frontend/backend) para reduzir exposição.
- Limites de CPU e memória para previsibilidade.
Conclusão: como era e como fica com uma abordagem correta
Antes de uma containerização bem planejada, o comum é encontrar builds pesados, dependências misturadas, segredos em arquivos copiados para a imagem e execução como root. Também aparece a diferença entre o que funciona localmente e o que quebra em produção, por falta de paridade e configuração clara. Nesse cenário, o Docker vira apenas mais uma camada de complexidade.
Com Multi-Stage Build, separação de ambientes no Compose, variáveis de ambiente bem tratadas e Nginx na frente, a aplicação passa a ter um formato consistente e operacional. A imagem final fica menor, o runtime fica mais seguro e a inicialização fica mais confiável por meio de entrypoint e healthcheck. O resultado é um deploy reproduzível, com serviços bem definidos e comportamento previsível do desenvolvimento até a produção.