Visão geral


A aplicação combina duas abordagens complementares: uma arquitetura em camadas (layered architecture) na infraestrutura e o padrão MVT (Model, View, Template) dentro do Django.

Na camada de infraestrutura, a separação entre proxy reverso, servidor de aplicação e banco de dados garante isolamento de responsabilidades. O Docker garante que essa arquitetura seja definida de forma explícita e executada de maneira consistente em qualquer ambiente.
Cada componente exerce uma função específica: o Caddy lida com tráfego público e HTTPS, o Gunicorn executa a aplicação, o Django concentra a lógica e o PostgreSQL persiste os dados. Essa divisão torna o sistema mais previsível, facilita manutenção e permite escalar ou substituir partes individualmente sem impactar o restante.

Dentro da aplicação, o padrão MVT organiza o código de forma clara. Models representam os dados, Views encapsulam a lógica e Templates cuidam da renderização. Essa separação reduz acoplamento, melhora a legibilidade e facilita evolução do sistema ao longo do tempo.

Primeiro — o que eu queria construir — e por quê?


Desde que comecei a estudar e praticar programação, há cerca de dois anos e meio, senti a necessidade de documentar boa parte do que aprendia. Normalmente, eu escrevia em editores de texto e exportava o PDF, para reler quando necessário.

Com o tempo, alguns problemas começaram a aparecer (além do inevitável tema claro em editores e PDFs): falta de organização prática, um workflow pouco confortável de escrita e leitura e dificuldade para manter consistência entre versões.

A motivação principal era escrever e ler sem fricção. Eu precisava de algo que me proporcionasse isso de forma estruturada e principalmente sob meu controle.

Foi assim que decidi criar um blog técnico, voltado à engenharia de software.

Tecnologias utilizadas


Antes de mais nada, vale listar as tecnologias utilizadas. Isso ajuda a ter uma visão geral da estrutura do sistema e de como seus componentes se conectam.

Stack

  • Infraestrutura: Docker
  • Linguagem: Python
  • Banco de dados: PostgreSQL
  • Framework: Django

  • Servidor de aplicação: Gunicorn

  • Servidor web (reverse proxy): Caddy

  • Frontend: Tailwind CSS e Django Templates

Componentes auxiliares

  • Edição de conteúdo: Summernote e Markdown

  • Segurança: Django Axes, middleware do Django e Bleach

  • Processamento de mídia: Pillow

  • Arquivos estáticos: Caddy e WhiteNoise

O início da infraestrutura — Docker


Contexto

Algum tempo atrás, uma colega de trabalho comentou sobre dificuldades recorrentes com ambientes de desenvolvimento. Dependências que não instalavam corretamente, configurações que quebravam com frequência e inconsistências entre máquinas.

Os problemas envolviam versões de banco de dados, conflitos de portas e diferenças nas versões da linguagem.

Na época, eu já havia tido algum contato com Docker, mas foi nesse momento que decidi levar a ferramenta mais a sério. Ela estava descrevendo exatamente o tipo de problema que o Docker se propõe a resolver.

A decisão

A primeira decisão foi usar o Docker como base da infraestrutura do projeto.

Containerizar tudo me permitiu ter um ambiente totalmente reprodutível em qualquer sistema que suporte Docker.

O objetivo era reduzir problemas com dependências e manter uma infraestrutura explicitamente organizada, que pudesse escalar e ser mantida sem dor de cabeça. Versões de banco de dados, Python e demais dependências ficam todas definidas de forma clara no ambiente.

É extremamente satisfatório migrar para um servidor e, com um simples docker compose up --build, ver o sistema ser construído passo a passo, sem erros de dependência e sem necessidade de configurações manuais.

Vale ressaltar que o Docker não remove a complexidade por permitir executar tudo com um único comando. Ele apenas desloca essa complexidade da configuração manual para arquivos declarativos como Dockerfile, docker-compose.yml e scripts de entrypoint.
Ou seja, a maior parte da infraestrutura fica declarada nesses arquivos, tornando o ambiente explícito e reproduzível.

Docker Compose

O projeto utiliza o Docker Compose para orquestrar três serviços: o banco de dados (PostgreSQL), a aplicação (Python e Django) e o reverse proxy (Caddy). Cada um possui uma função específica e se comunica através da rede interna do Docker.

Para que docker compose up --build seja o único passo necessário após clonar o repositório, toda a configuração é executada automaticamente na inicialização dos containers. Isso inclui a instalação de dependências, o build do CSS com Tailwind e a execução das migrações do banco de dados.

A base de dados — PostgreSQL


Um banco de dados relacional é a escolha correta para modelar meu blog, já que a estrutura dos dados é naturalmente relacional, com entidades bem definidas (Post, Category e Tag) e relações claras entre elas. Nesse contexto, PostgreSQL foi o candidato escolhido, além de ser o banco de dados padrão das aplicações Django.

A base de dados persiste em um volume nomeado, gerenciado pelo Docker. Os dados ficam armazenados no filesystem do host, mas são abstraídos pelo Docker, sem necessidade de intervenção manual. A escolha de um volume nomeado foi a opção mais adequada nesse cenário, garantindo persistência independente do ciclo de vida dos containers. Outras abordagens como volumes externos (NFS), adicionariam complexidade desnecessária para o escopo do projeto.

O container de aplicação só inicializa após o PostgreSQL estar totalmente pronto para aceitar conexões. Isso é feito por meio de uma conexão de teste no script de entrypoint da aplicação. Esse detalhe é importante, pois evita race conditions durante o processo de inicialização entre serviços.

Django Framework


Python é minha linguagem de programação principal, então Django naturalmente já fazia parte da minha stack de desenvolvimento web. Ainda assim, o framework foi escolhido principalmente por fornecer tudo que uma aplicação orientada a conteúdo precisa, sem exigir a construção desses componentes do zero; como o próprio projeto define, Django é batteries included:

  • ORM
  • Interface administrativa
  • Sistema de templates
  • Roteamento de URLs
  • Autenticação
  • Middleware
  • Sistema de migrações de banco de dados

Especificamente para um blog, a interface administrativa do Django é particularmente valiosa. Ela fornece uma forma de gerenciar o conteúdo sem a necessidade de desenvolver um frontend customizado para essas atividades.

A aplicação possui três models principais: Post, Category e Tag. Posts têm uma relação many-to-one com Category (uma categoria por post) e uma relação many-to-many com Tag (múltiplas tags por post). Esses relacionamentos são gerenciados pelo ORM do Django, que abstrai a construção de consultas SQL a partir das relações entre objetos Python.

Como eu escreveria de forma confortável?


Como introduzido no início do post, meu objetivo é escrever e ler sem fricção. Portanto, é de suma importância que a escrita seja confortável, customizável, padronizada e rápida. Devido a projetos de estudos anteriores em Django, eu conhecia o editor de texto Summernote.

Para maior praticidade e versatilidade, também optei por incluir suporte à escrita em Markdown. Para desenvolvedores, é uma forma mais rápida e precisa de estruturar conteúdo em comparação a um editor WYSIWYG como o Summernote, além de permitir escrita fora da aplicação, em qualquer editor de texto.

Detalhes técnicos dessas escolhas

Todo conteúdo, independentemente da forma de escrita, é convertido para HTML antes de ser armazenado. Isso significa que posts escritos em Markdown possuem uma versão renderizada em HTML, que é a utilizada na aplicação.

Antes de serem persistidos, os posts passam por um processo de sanitização com a biblioteca Bleach, que permite apenas um conjunto específico de tags HTML definidas previamente. Isso garante que o conteúdo armazenado seja seguro para renderização direta nos templates.

Um detalhe importante é que o fluxo não é simétrico: enquanto Markdown é convertido para HTML, o HTML gerado pelo Summernote não é convertido de volta para Markdown. Ou seja, nem todo conteúdo possui uma representação em Markdown, mas todo conteúdo possui uma representação em HTML. Por este motivo, eu sempre priorizo a escrita em Markdown, e só depois realizo ajustes via Summernote caso necessário.

Summernote

Summernote é um editor WYSIWYG (what you see is what you get) integrado ao admin do Django. Ele permite escrever e formatar conteúdo diretamente pela interface administrativa, sem a necessidade de lidar com HTML manualmente (embora seja possível com a funcionalidade code view). Ele também elimina a necessidade de ferramentas externas e reduz o atrito entre escrever e publicar.

Como o Summernote se integra naturalmente ao Django Admin, evita-se a necessidade de desenvolver uma interface customizada para edição de conteúdo.

Entretanto, pra mim, o maior diferencial é uma das funcionalidades que ele oferece: o upload de imagens, que são armazenadas no servidor e referenciadas por URL no conteúdo do post.

Redimensionamento de imagens

Para evitar imagens maiores do que o necessário, antes de serem armazenadas, um módulo auxiliar de redimensionamento, utilizando a biblioteca Pillow, é acionado via signals do Django, processando a imagem antes de sua persistência.

Markdown

Markdown é uma linguagem de marcação leve que permite escrever conteúdo em texto puro, posteriormente convertido em HTML para renderização. Para desenvolvedores, isso torna a escrita mais rápida e precisa, especialmente para conteúdos técnicos.

A principal motivação para incluir suporte a Markdown foi permitir a escrita fora da aplicação, em editores como o Neovim (ou qualquer outro de preferência), sem dependência de interfaces ou ambientes específicos.

Trade-offs

O Summernote é um editor de texto amigável, já que funções como itálico, cabeçalhos, inserção de imagens e links são acessadas de forma visual, como em editores modernos. Entretanto, isso torna a escrita menos fluida e mais lenta, devido à necessidade de interação constante com a interface, além de reduzir o controle sobre o HTML gerado.

O Markdown, por outro lado, é mais leve, previsível, rápido e portável.

Sua principal limitação é a ausência de um mecanismo nativo para upload de imagens. Diferente do Summernote, que realiza o upload automaticamente no momento da inserção, no Markdown esse processo precisa ser feito separadamente, com a imagem sendo referenciada manualmente por URL no conteúdo. Para casos que envolvem mídia, é ideal complementar ou editar o post posteriormente via Summernote.

Servidor de aplicação — Gunicorn


O Gunicorn (Green Unicorn) é um servidor WSGI para aplicações Python, amplamente utilizado para executar aplicações Django em produção. Ele gerencia requisições concorrentes por meio de múltiplos processos worker.

O Gunicorn não deve ser exposto diretamente à internet. Ele escuta em uma porta interna, e todo o tráfego chega até ele por meio de um proxy reverso.

Ele atua como a camada de execução da aplicação, enquanto responsabilidades como HTTPS, roteamento e entrega de arquivos estáticos são delegadas ao proxy reverso.

Proxy reverso — Caddy


O Caddy fica na frente do Gunicorn e gerencia todo o tráfego público. Seu papel é receber as requisições vindas da internet e encaminhá-las para o destino correto.

A utilização de um proxy reverso é necessária porque o servidor de aplicação (Gunicorn) não deve ser exposto diretamente à internet. Ele é responsável apenas por executar a aplicação, não por lidar com todas as responsabilidades de um servidor web público.

O Caddy automatiza três aspectos importantes que, sem ele, exigiriam configuração adicional significativa:

  • Terminação HTTPS
  • Redirecionamento de HTTP para HTTPS
  • Gerenciamento de certificados SSL via Let's Encrypt

Arquivos estáticos

Além disso, o Caddy pode servir arquivos de mídia diretamente, sem passar pelo Django. Já os arquivos estáticos, como CSS e fontes, são servidos pelo WhiteNoise, uma biblioteca Python que lida com arquivos estáticos de forma eficiente dentro do processo da aplicação.

Frontend


Framework CSS — Tailwind

O frontend utiliza o framework Tailwind CSS, um framework utilitário onde os estilos são aplicados diretamente no HTML por meio de classes pré-definidas. Em vez de escrever CSS customizado para cada componente, layout e estilos são compostos diretamente nas marcações HTML.

O Tailwind é construído dentro do container Docker durante a inicialização. O toolchain do Node.js é instalado na imagem da aplicação, e o processo de build do CSS é executado como parte do startup do container. O arquivo CSS compilado é gerado a cada inicialização.

Modo claro e escuro

O blog permite alternar entre modo claro e escuro. A preferência do usuário é armazenada no local storage do navegador e aplicada a cada carregamento da página. O toggle altera a classe dark no elemento raiz do HTML, ativando as variações de estilo definidas no Tailwind.

Syntax Highlighting

Para blocos de código, o syntax highlighting é fornecido pela biblioteca JavaScript Highlight.js, carregada apenas nas páginas que exibem conteúdo técnico.

Fontes

As fontes são self-hosted. Em vez de serem carregadas de servidores externos, como os da Google, são servidas pelo mesmo servidor da aplicação. Isso remove uma dependência externa, reduz o número de requisições e evita DNS lookups adicionais, além de eliminar implicações de privacidade associadas a terceiros.

Segurança


Proteção contra ataques brute-force

Brute-force é um tipo de ataque em que um usuário malicioso tenta, de forma programática, múltiplas combinações de senha até conseguir realizar login no sistema.

Para proteção contra esse tipo de ataque na página administrativa de login, utilizei o django-axes, que rastreia tentativas de autenticação e bloqueia um endereço de IP após um número configurável de falhas.

HTTPS e Middleware

HTTPS é obrigatório; todas as requisições HTTP são automaticamente redirecionadas para HTTPS pelo Caddy. Os middlewares de segurança do Django adicionam cabeçalhos que instruem o navegador a se conectar via HTTPS no futuro (HSTS), impedem MIME type sniffing e evitam que a aplicação seja incorporada em iframes de outros domínios.

Sanitização de conteúdo — XSS

XSS (Cross-Site Scripting) é um tipo de ataque em que código malicioso é injetado em conteúdo que será renderizado por outros usuários. Quando a página é carregada, esse código é executado no contexto do navegador da vítima, podendo acessar cookies, manipular o DOM ou realizar ações autenticadas.

Embora eu seja o único autor dos posts, a sanitização de conteúdo foi implementada com a biblioteca Bleach. Apenas um conjunto previamente definido de tags HTML é permitido, impedindo que código JavaScript não permitido seja injetado.

Proteção contra ataques CSRF

CSRF (Cross-Site Request Forgery) é um tipo de ataque em que um site malicioso induz o navegador do usuário a executar requisições autenticadas em outro sistema. A proteção é feita por meio de tokens únicos, validados em cada requisição, garantindo que apenas ações iniciadas pela própria aplicação sejam aceitas.

A proteção contra CSRF é fornecida pelo middleware do Django, que valida um token em toda requisição POST. Os cookies de sessão e os tokens CSRF são marcados como seguros, sendo transmitidos apenas via HTTPS.

Backup


Não faria sentido construir um sistema para escrita, leitura, que mantém um registro cronológico dos posts, correndo o risco de perder todo o trabalho realizado. Por isso, o backup dos posts foi uma preocupação desde o início.

Entre as possíveis abordagens, optei por utilizar os signals do Django combinados com lógica própria.

Backup orientado a eventos

O sistema de backup é orientado a eventos, utilizando signals do Django para reagir automaticamente a alterações no banco de dados.

Ao salvar um post, o signal post_save dispara a criação de um snapshot inicial ou de um backup incremental, dependendo do contexto. No caso de criação, o sistema gera um snapshot completo do conteúdo, incluindo a mídia associada. Em atualizações, apenas novos recursos (como imagens) são armazenados, criando um histórico versionado ao longo do tempo.

Esse modelo permite reconstruir qualquer versão de um post, mantendo tanto o conteúdo quanto seus recursos associados de forma consistente.

Problema no backup das tags

Relacionamentos many-to-many (tags) são persistidos separadamente no Django, o que inicialmente não foi considerado durante a implementação do sistema de backup. Como consequência, um segundo signal (m2m_changed) era disparado após a criação do post, fazendo com que todo post novo gerasse automaticamente um snapshot inicial seguido de um update.

Para garantir consistência, esse segundo signal passou a ser tratado explicitamente. Quando o post é recém-criado (dentro de um intervalo curto de tempo), o evento de M2M é tratado como parte da criação, sobrescrevendo o snapshot inicial. Em casos de edição real, um novo backup incremental é gerado.

Backup do banco

Como camada adicional de segurança, são realizados periodicamente dumps do banco de dados utilizando pg_dump. Essa abordagem fornece redundância ao sistema de backup, permitindo a restauração completa do estado da aplicação a partir de um arquivo .sql.

Fluxo de uma requisição


Uma requisição ao sistema segue o seguinte fluxo:
Browser → Caddy → Gunicorn → Django → ORM → PostgreSQL → ORM → Django → Gunicorn → Caddy → Browser

O navegador envia a requisição principal, que é recebida pelo Caddy e encaminhada ao Gunicorn. Após receber o HTML da resposta, o navegador realiza requisições adicionais para arquivos estáticos e de mídia, que são servidos diretamente pelo Caddy, sem passar pela aplicação.

No fluxo da requisição principal, o Gunicorn delega a requisição a um worker, que a repassa para a aplicação Django. Dentro do Django, a requisição percorre a cadeia de middlewares, é resolvida pelo roteador de URLs e processada por uma view, que consulta o banco de dados via ORM (quando necessário) e renderiza o template da resposta.

A resposta é então construída e enviada de volta pelo mesmo caminho até o cliente.

Requisições de recursos

Como descrito acima, a requisição principal retorna o HTML ao cliente. A partir desse HTML, o navegador identifica referências a recursos adicionais e realiza novas requisições (subresource requests) para arquivos como CSS, JavaScript, imagens e fontes.

Essas requisições são tratadas diretamente pelo Caddy quando se referem a arquivos estáticos ou de mídia, sem passar pela aplicação Django, o que reduz a carga no servidor de aplicação e melhora o desempenho geral.

Considerações finais


A construção do LearningSea, do início até a produção, me ensinou muito sobre como tomar decisões conscientes em cada camada do sistema, na prática.

Cada escolha, desde o uso do Docker até a adoção do Django, passando pela separação entre proxy, servidor de aplicação e banco de dados, foi guiada por previsibilidade, controle e simplicidade operacional. O objetivo nunca foi criar algo excessivamente complexo, mas sim um sistema compreensível, reproduzível e fácil de evoluir.

Ao longo do processo, alguns pontos se mostraram mais importantes do que o esperado, como a necessidade de garantir consistência entre serviços, lidar com detalhes de persistência (como relacionamentos many-to-many), pensar em estratégias de backup desde o início e considerar aspectos de segurança em diferentes partes do sistema. Esses fatores, embora menos visíveis, são fundamentais para a confiabilidade da aplicação.

Mais do que o produto final, o valor está no processo: entender como cada parte se conecta e quais são os trade-offs envolvidos em cada decisão. É esse entendimento que permite evoluir o sistema com segurança, sem depender de tentativa e erro.


Pensando nisso, os próximos posts irão explorar cada uma dessas camadas com mais profundidade, detalhando as decisões e os aspectos de implementação.