Python

Templates HTML com Jinja2 e Deploy com Docker Já leu

10 min de leitura

Templates HTML com Jinja2 e Deploy com Docker
Até aqui construímos APIs que retornam JSON. Mas muitas aplicações precisam também servir HTML — painéis administrativos, relatórios, páginas de erro customizadas. Jinja2 é o motor de templates padrão do Flask e do FastA

Até aqui construímos APIs que retornam JSON. Mas muitas aplicações precisam também servir HTML — painéis administrativos, relatórios, páginas de erro customizadas. Jinja2 é o motor de templates padrão do Flask e do FastAPI, e dominar seus recursos abre essa porta. Na segunda parte do artigo veremos como empacotar toda a aplicação em um container Docker — o padrão da indústria para deploy consistente e reproduzível.


Jinja2: Motor de Templates

pip install jinja2

Sintaxe Fundamental

<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <title>{% block titulo %}Sistema Escolar{% endblock %}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/estilo.css') }}">
</head>
<body>
    <nav>
        <a href="{{ url_for('inicio') }}">Início</a>
        <a href="{{ url_for('alunos.listar') }}">Alunos</a>
        {% if usuario %}
            <span>Olá, {{ usuario.nome }}!</span>
            <a href="{{ url_for('auth.logout') }}">Sair</a>
        {% else %}
            <a href="{{ url_for('auth.login') }}">Entrar</a>
        {% endif %}
    </nav>

    <main>
        {% with mensagens = get_flashed_messages(with_categories=true) %}
            {% for categoria, mensagem in mensagens %}
                <div class="alerta alerta-{{ categoria }}">{{ mensagem }}</div>
            {% endfor %}
        {% endwith %}

        {% block conteudo %}{% endblock %}
    </main>

    <footer>
        <p>&copy; {{ ano }} Sistema Escolar</p>
    </footer>
</body>
</html>
<!-- templates/alunos/lista.html -->
{% extends "base.html" %}

{% block titulo %}Lista de Alunos{% endblock %}

{% block conteudo %}
<h1>Alunos</h1>

<p>Total: {{ alunos | length }}</p>

<table>
    <thead>
        <tr>
            <th>#</th>
            <th>Nome</th>
            <th>Nota</th>
            <th>Status</th>
        </tr>
    </thead>
    <tbody>
        {% for aluno in alunos %}
        <tr class="{{ 'aprovado' if aluno.nota >= 6 else 'reprovado' }}">
            <td>{{ loop.index }}</td>
            <td>{{ aluno.nome | title }}</td>
            <td>{{ "%.1f" | format(aluno.nota) }}</td>
            <td>
                {% if aluno.nota >= 9 %}
                    Excelente
                {% elif aluno.nota >= 7 %}
                    Bom
                {% elif aluno.nota >= 6 %}
                    Regular
                {% else %}
                    Reprovado
                {% endif %}
            </td>
        </tr>
        {% else %}
            <tr><td colspan="4">Nenhum aluno cadastrado.</td></tr>
        {% endfor %}
    </tbody>
</table>
{% endblock %}

Filtros e Funções Jinja2

<!-- Filtros embutidos -->
{{ nome | upper }}                  <!-- RICARDO -->
{{ nome | lower }}                  <!-- ricardo -->
{{ nome | title }}                  <!-- Ricardo Matos -->
{{ nome | truncate(20) }}           <!-- Ricardo Matos... -->
{{ lista | length }}                <!-- 5 -->
{{ lista | join(", ") }}            <!-- a, b, c -->
{{ valor | round(2) }}              <!-- 9.50 -->
{{ texto | replace("a", "b") }}
{{ html | safe }}                   <!-- renderiza HTML sem escapar -->
{{ data | strftime("%d/%m/%Y") }}   <!-- filtro customizado -->

<!-- Testes -->
{% if valor is none %}
{% if lista is iterable %}
{% if numero is divisibleby(2) %}
{% if texto is defined %}

Filtros Customizados

# app.py — registrando filtros customizados no Flask
from datetime import datetime

@app.template_filter("data_br")
def filtro_data_br(valor):
    if isinstance(valor, str):
        valor = datetime.fromisoformat(valor)
    return valor.strftime("%d/%m/%Y")

@app.template_filter("moeda")
def filtro_moeda(valor):
    return f"R$ {valor:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")

@app.template_filter("plural")
def filtro_plural(numero, singular, plural):
    return singular if numero == 1 else plural
<!-- Usando filtros customizados -->
{{ aluno.criado_em | data_br }}         <!-- 15/03/2024 -->
{{ produto.preco   | moeda }}           <!-- R$ 1.299,90 -->
{{ total | plural("aluno", "alunos") }} <!-- 1 aluno / 5 alunos -->

Templates com Flask

from flask import Flask, render_template, request, redirect, url_for, flash
from datetime import datetime

app = Flask(__name__)
app.secret_key = "dev-secret"

alunos_dados = [
    {"id": 1, "nome": "Ana Silva",   "nota": 9.5},
    {"id": 2, "nome": "Bruno Costa", "nota": 7.0},
    {"id": 3, "nome": "Carla Souza", "nota": 5.5},
]


@app.context_processor
def variaveis_globais():
    """Variáveis disponíveis em todos os templates."""
    return {
        "ano":     datetime.now().year,
        "usuario": None   # em produção: sessão do usuário logado
    }


@app.route("/")
def inicio():
    return render_template("inicio.html")


@app.route("/alunos")
def listar_alunos():
    nota_min = request.args.get("nota_min", 0, type=float)
    filtrados = [a for a in alunos_dados if a["nota"] >= nota_min]
    return render_template(
        "alunos/lista.html",
        alunos=filtrados,
        nota_min=nota_min
    )


@app.route("/alunos/novo", methods=["GET", "POST"])
def novo_aluno():
    if request.method == "POST":
        nome = request.form.get("nome", "").strip()
        nota = request.form.get("nota", type=float)

        if not nome or nota is None:
            flash("Nome e nota são obrigatórios.", "erro")
            return render_template("alunos/form.html")

        aluno = {"id": len(alunos_dados) + 1, "nome": nome, "nota": nota}
        alunos_dados.append(aluno)
        flash(f"Aluno '{nome}' cadastrado com sucesso!", "sucesso")
        return redirect(url_for("listar_alunos"))

    return render_template("alunos/form.html")

Templates com FastAPI

pip install jinja2 python-multipart
from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

app       = FastAPI()
templates = Jinja2Templates(directory="templates")

app.mount("/static", StaticFiles(directory="static"), name="static")


@app.get("/", response_class=HTMLResponse)
async def inicio(request: Request):
    return templates.TemplateResponse(
        "inicio.html",
        {"request": request, "titulo": "Sistema Escolar"}
    )


@app.get("/alunos", response_class=HTMLResponse)
async def listar_alunos(request: Request):
    return templates.TemplateResponse(
        "alunos/lista.html",
        {
            "request": request,
            "alunos":  list(_alunos.values()),
            "ano":     datetime.now().year
        }
    )


@app.post("/alunos/novo")
async def criar_aluno_form(
    request: Request,
    nome:  str   = Form(...),
    email: str   = Form(...),
    nota:  float = Form(...)
):
    # Processa o formulário e redireciona
    return RedirectResponse(url="/alunos", status_code=303)

Docker: Containerizando a Aplicação

Docker empacota a aplicação com todas as suas dependências em uma unidade isolada — o container — que roda de forma idêntica em qualquer ambiente.

# Instalação — https://docs.docker.com/get-docker/
docker --version
docker compose version

Dockerfile para FastAPI

# Dockerfile
FROM python:3.12-slim

# Variáveis de ambiente
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

# Diretório de trabalho
WORKDIR /app

# Instala dependências primeiro (aproveitando cache de camadas)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copia o código
COPY . .

# Usuário não-root — boa prática de segurança
RUN adduser --disabled-password --gecos "" appuser && \
    chown -R appuser:appuser /app
USER appuser

# Porta exposta
EXPOSE 8000

# Comando de inicialização
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# .dockerignore
__pycache__/
*.pyc
*.pyo
.env
.git/
.gitignore
venv/
.venv/
tests/
*.md
.pytest_cache/
# Construindo a imagem
docker build -t escola-api:1.0.0 .

# Rodando o container
docker run -d \
  --name escola-api \
  -p 8000:8000 \
  -e SECRET_KEY=minha-chave-secreta \
  -e DATABASE_URL=postgresql://user:senha@host/db \
  escola-api:1.0.0

# Verificando logs
docker logs -f escola-api

# Parando e removendo
docker stop escola-api
docker rm escola-api

Docker Compose: Orquestrando Múltiplos Serviços

# docker-compose.yml
version: "3.9"

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: escola-api
    ports:
      - "8000:8000"
    environment:
      - SECRET_KEY=${SECRET_KEY}
      - DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@db:5432/escola
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    volumes:
      - ./logs:/app/logs
    restart: unless-stopped
    networks:
      - escola-net

  db:
    image: postgres:16-alpine
    container_name: escola-db
    environment:
      - POSTGRES_DB=escola
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - escola-net

  redis:
    image: redis:7-alpine
    container_name: escola-redis
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    networks:
      - escola-net

  nginx:
    image: nginx:alpine
    container_name: escola-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - api
    networks:
      - escola-net

volumes:
  postgres_data:
  redis_data:

networks:
  escola-net:
    driver: bridge
# nginx/nginx.conf
upstream api {
    server api:8000;
}

server {
    listen 80;
    server_name meusite.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name meusite.com;

    ssl_certificate     /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;

    location / {
        proxy_pass         http://api;
        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_read_timeout 60s;
    }

    location /static {
        alias /app/static;
        expires 7d;
    }
}
# Arquivo .env para docker-compose
SECRET_KEY=sua-chave-super-secreta-aqui
DB_PASSWORD=senha-do-postgres
REDIS_PASSWORD=senha-do-redis
# Comandos docker compose
docker compose up -d          # sobe todos os serviços em background
docker compose down           # para e remove containers
docker compose logs -f api    # logs em tempo real do serviço api
docker compose ps             # status dos serviços
docker compose exec api bash  # acessa o container da api
docker compose build --no-cache  # reconstrói as imagens

Dockerfile Multi-stage: Imagem Otimizada

# Dockerfile.prod — imagem de produção otimizada
FROM python:3.12-slim AS builder

WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt


FROM python:3.12-slim AS runtime

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

# Copia apenas as dependências instaladas
COPY --from=builder /install /usr/local

# Copia o código da aplicação
COPY app/ ./app/
COPY templates/ ./templates/
COPY static/ ./static/

# Usuário não-root
RUN adduser --disabled-password --gecos "" appuser
USER appuser

EXPOSE 8000

CMD ["uvicorn", "app.main:app", \
     "--host", "0.0.0.0", \
     "--port", "8000", \
     "--workers", "4"]

O build multi-stage reduz o tamanho final da imagem significativamente — o stage builder instala compiladores e ferramentas que não precisam ir para produção.


Health Check e Graceful Shutdown

from fastapi import FastAPI
from contextlib import asynccontextmanager
import asyncio


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup
    print("Conectando ao banco...")
    yield
    # Shutdown — aguarda requisições em andamento
    print("Encerrando conexões...")
    await asyncio.sleep(1)


app = FastAPI(lifespan=lifespan)


@app.get("/health")
async def health_check():
    """Endpoint para verificação de saúde — usado pelo Docker e load balancer."""
    return {
        "status":  "healthy",
        "versao":  "1.0.0",
    }


@app.get("/ready")
async def readiness_check():
    """Verifica se a aplicação está pronta para receber tráfego."""
    try:
        # Verifica conexão com banco, cache, etc.
        return {"status": "ready"}
    except Exception:
        from fastapi import HTTPException
        raise HTTPException(status_code=503, detail="Serviço indisponível.")

Resumo

  • Jinja2 separa lógica de apresentação com herança de templates, filtros e variáveis de contexto
  • {% block %} permite herança; {% for %} e {% if %} controlam fluxo no template
  • Filtros customizados formatam dados específicos do domínio da aplicação
  • context_processor no Flask injeta variáveis globais em todos os templates
  • Docker empacota a aplicação e suas dependências em containers portáveis e reproduzíveis
  • O Dockerfile define como construir a imagem; .dockerignore exclui arquivos desnecessários
  • Docker Compose orquestra múltiplos serviços — API, banco, cache e proxy reverso juntos
  • Build multi-stage produz imagens menores separando ambiente de build do de runtime
  • Endpoints /health e /ready permitem que orquestradores monitorem o estado da aplicação

Referências e Leituras Complementares

  • Jinja2 — documentação oficial — https://jinja.palletsprojects.com/en/3.1.x/
  • FastAPI — templates — https://fastapi.tiangolo.com/advanced/templates/
  • Docker — documentação oficial — https://docs.docker.com/
  • Docker Compose — referência — https://docs.docker.com/compose/compose-file/
  • Nginx como proxy reverso — https://nginx.org/en/docs/beginners_guide.html
  • Python Docker official images — https://hub.docker.com/_/python
  • KANE, Sean; MATTHIAS, Karl. Docker: Up and Running. 3. ed. O'Reilly Media, 2023. — guia completo de Docker para desenvolvimento e produção.
  • GRINBERG, Miguel. Flask Web Development. 2. ed. O'Reilly Media, 2018. Cap. 3 — templates com Jinja2 no contexto do Flask.
Comentários

Mais em Python

Interfaces, Protocolos e Composição
Interfaces, Protocolos e Composição

No artigo anterior vimos que heran&ccedil;a &eacute; uma ferramenta poderosa...

Artigo 44 — Qualidade de Código: linting, type hints e mypy
Artigo 44 — Qualidade de Código: linting, type hints e mypy

Testes verificam o que o código faz. Ferramentas de qualidade verificam como...

MySQL, MariaDB, PostgreSQL e MongoDB
MySQL, MariaDB, PostgreSQL e MongoDB

No artigo anterior trabalhamos com SQLite — excelente para desenvolvimento lo...