FastAPI em Produção com Docker e UV: Arquitetura Senior, Padrões Escaláveis e Boas Práticas em 2026

Published on: 2025-12-25
Post image
pt fastapi docker uv backend-em-producao arquitetura-backend fastapi-production async-python sqlalchemy-async pydantic-v2 docker-compose uvicorn uvloop microservicos api-escalavel boas-praticas-backend python-moderno

Um backend moderno em Python costuma combinar desenvolvimento rápido com disciplina de produção, evitando acoplamento excessivo, configurações frágeis e containers pesados. Nesse contexto, FastAPI (framework web assíncrono), Docker (empacotamento e execução isolada) e UV (gerenciador de dependências e ambientes) formam uma base sólida para APIs com tráfego alto.

Um conjunto de padrões “senior” organiza o projeto em camadas, padroniza configuração por variáveis de ambiente, separa regras de negócio de acesso a dados e prepara o deploy com build em múltiplas etapas. O resultado é uma aplicação mais testável, escalável e previsível, com inicialização controlada e observabilidade melhor.

Estrutura de projeto pronta para produção (arquitetura em camadas)

Uma estrutura de pastas orientada a camadas distribui responsabilidades de forma explícita e reduz dependências circulares. A camada de rotas concentra apenas detalhes HTTP, enquanto a camada de serviço contém as regras de negócio. A camada de repositório padroniza o acesso ao banco e isola a biblioteca de persistência. Uma pasta central de núcleo reúne configuração, banco, segurança, logs e exceções comuns.

Esse desenho melhora manutenção porque mudanças em um domínio (por exemplo, usuários) ficam contidas em um módulo. Também facilita testes, pois serviços e repositórios podem ser testados separadamente. A organização favorece “crescimento por módulos”, em vez de um único arquivo grande. Em produção, esse padrão reduz risco de regressões ao tornar as fronteiras mais claras.

A estrutura a seguir mostra um exemplo completo e típico de equipes experientes em APIs. Ela separa infraestrutura, componentes compartilhados, módulos de domínio e testes. O objetivo é deixar óbvio onde cada coisa deve ficar e por quê. A listagem apresenta uma visão geral dos diretórios principais.

  • src/core: configuração, banco, segurança, logging e exceções globais.
  • src/common: dependências e utilitários compartilhados (paginação, tipos e schemas-base).
  • src/users e src/auth: módulos de domínio (router, schemas, models, repository, service).
  • tests: testes unitários e de integração com fixtures e fábricas.
  • alembic: migrações de banco (versionamento de schema).
  • Dockerfile e docker-compose: empacotamento e orquestração.

Dependências modernas com pyproject.toml e UV

O arquivo pyproject.toml centraliza metadados do projeto e dependências, substituindo formatos antigos. O UV gerencia instalação e resolve versões com rapidez, mantendo um arquivo uv.lock com versões travadas. Em produção, travar versões evita diferenças entre ambientes e reduz falhas inesperadas. Em desenvolvimento, grupos de dependências ajudam a separar ferramentas como testes e lint.

O bloco [project] define dependências de runtime, como FastAPI, Pydantic, SQLAlchemy e asyncpg. O bloco [dependency-groups] (ou equivalente) guarda dependências de desenvolvimento, como pytest, ruff e mypy. O bloco [tool.uv] pode declarar dependências de desenvolvimento para o UV. Isso garante reprodutibilidade e builds mais previsíveis.

Além disso, ferramentas de qualidade podem ser configuradas no mesmo arquivo. Ruff atua como linter e formatador, mantendo estilo consistente e reduzindo falhas comuns. MyPy adiciona checagem estática de tipos, útil quando a API cresce e integra mais módulos. A configuração “strict” aumenta segurança, exigindo tipagem mais completa.

Um exemplo profissional de configuração em pyproject.toml aparece abaixo. Ele inclui dependências, grupos de desenvolvimento e configurações de Ruff, Pytest e MyPy em um único lugar. Esse padrão é amplamente usado para padronização de repositórios. O conteúdo a seguir representa um arquivo realista e completo.

[project]
name = "my-fastapi-app"
version = "0.1.0"
description = "Aplicação FastAPI para produção"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "fastapi[standard]>=0.115.0",
    "pydantic>=2.9",
    "pydantic-settings>=2.5",
    "sqlalchemy[asyncio]>=2.0",
    "asyncpg>=0.30.0",
    "alembic>=1.14.0",
    "python-jose[cryptography]>=3.3.0",
    "passlib[bcrypt]>=1.7.4",
    "httpx>=0.28.0",
    "structlog>=24.0.0",
    "tenacity>=9.0.0",
]

[dependency-groups]
dev = [
    "pytest>=8.0",
    "pytest-asyncio>=0.24.0",
    "pytest-cov>=6.0",
    "httpx>=0.28.0",
    "factory-boy>=3.3.0",
    "ruff>=0.8.0",
    "mypy>=1.13.0",
    "pre-commit>=4.0.0",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.uv]
dev-dependencies = [
    "pytest>=8.0",
    "pytest-asyncio>=0.24.0",
    "ruff>=0.8.0",
    "mypy>=1.13.0",
]

[tool.ruff]
target-version = "py312"
line-length = 88
indent-width = 4

[tool.ruff.lint]
select = [
    "E", "W", "F", "I", "B", "C4", "UP", "ARG", "SIM", "TCH", "PTH", "ERA", "RUF", "ASYNC",
]
ignore = [
    "E501",
    "B008",
]
fixable = ["ALL"]

[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
"tests/*" = ["ARG", "S101"]
"alembic/*" = ["ERA001"]

[tool.ruff.lint.isort]
known-first-party = ["src"]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"

[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
testpaths = ["tests"]
addopts = "-v --tb=short"

[tool.mypy]
python_version = "3.12"
strict = true
plugins = ["pydantic.mypy"]

[[tool.mypy.overrides]]
module = ["tests.*"]
disallow_untyped_defs = false

Configuração robusta com Pydantic Settings v2

Pydantic Settings valida configurações e carrega variáveis de ambiente com tipos corretos. Isso evita erros silenciosos, como portas como texto ou senhas vazias. O uso de env_nested_delimiter permite agrupar variáveis como POSTGRES__HOST, reduzindo confusão em ambientes complexos. O uso de cache com lru_cache evita reconstruções repetidas do objeto de configuração.

Campos como secret_key e credenciais do banco podem exigir tamanho mínimo, reduzindo risco de configurações fracas. Propriedades calculadas com computed_field geram URLs de conexão consistentes para runtime e para migrações. A flag de ambiente separa desenvolvimento, homologação e produção de forma explícita. Esse desenho segue a ideia de “configuração fora do código”, típica de aplicações 12-factor.

O arquivo abaixo exemplifica uma configuração completa, com validação e construção de URLs. A URL assíncrona é usada pela aplicação, enquanto a URL síncrona é usada por ferramentas como Alembic. O resultado é uma fonte única da verdade para parâmetros críticos. O trecho também mostra um alias para injeção de dependência.

from functools import lru_cache
from typing import Literal

from pydantic import AnyHttpUrl, Field, computed_field
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    """Configurações da aplicação com validação e carregamento de ambiente."""

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        case_sensitive=False,
        extra="ignore",
        env_nested_delimiter="__",
    )

    app_name: str = "FastAPI App"
    app_version: str = "0.1.0"
    debug: bool = False
    environment: Literal["development", "staging", "production"] = "development"

    api_v1_prefix: str = "/api/v1"

    postgres_user: str
    postgres_password: str = Field(..., min_length=8)
    postgres_host: str = "localhost"
    postgres_port: int = 5432
    postgres_db: str

    db_pool_size: int = 5
    db_max_overflow: int = 10
    db_pool_timeout: int = 30
    db_pool_recycle: int = 1800
    db_echo: bool = False

    @computed_field
    @property
    def database_url(self) -> str:
        """URL assíncrona para SQLAlchemy."""
        return (
            f"postgresql+asyncpg://{self.postgres_user}:{self.postgres_password}"
            f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}"
        )

    @computed_field
    @property
    def database_url_sync(self) -> str:
        """URL síncrona para migrações com Alembic."""
        return (
            f"postgresql+psycopg://{self.postgres_user}:{self.postgres_password}"
            f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}"
        )

    secret_key: str = Field(..., min_length=32)
    algorithm: str = "HS256"
    access_token_expire_minutes: int = 30
    refresh_token_expire_days: int = 7

    backend_cors_origins: list[AnyHttpUrl] = []

    @property
    def is_production(self) -> bool:
        return self.environment == "production"


@lru_cache
def get_settings() -> Settings:
    """Singleton em cache para as configurações."""
    return Settings()

Camada de banco com SQLAlchemy assíncrono (Async SQLAlchemy 2.0)

SQLAlchemy é uma biblioteca para mapear classes Python em tabelas e executar consultas. O modo assíncrono permite melhor uso de I/O em APIs com muitas conexões simultâneas, usando drivers como asyncpg. O engine representa o mecanismo de conexão e o pool é o conjunto de conexões reutilizáveis. Configurações como pool_pre_ping evitam conexões “mortas” em produção.

A sessão assíncrona representa uma unidade de trabalho, agrupando leituras e escritas com commit e rollback. O padrão com yield em dependências do FastAPI garante fechamento correto mesmo em exceções. O uso de expire_on_commit=False evita que objetos “expirem” após commit, algo importante no modo async. A função de dispose fecha recursos no shutdown, reduzindo vazamentos.

O código a seguir mostra uma implementação típica de engine, sessionmaker e dependência de sessão. Ele também inclui funções utilitárias para criar tabelas em desenvolvimento e finalizar o engine. O alias de dependência simplifica assinaturas de endpoints e serviços. O trecho representa uma base sólida para um projeto escalável.

from collections.abc import AsyncGenerator

from fastapi import Depends
from sqlalchemy.ext.asyncio import (
    AsyncSession,
    async_sessionmaker,
    create_async_engine,
)
from sqlalchemy.orm import DeclarativeBase
from typing import Annotated

from src.core.config import get_settings

settings = get_settings()


class Base(DeclarativeBase):
    """Classe base para todos os modelos SQLAlchemy."""


engine = create_async_engine(
    settings.database_url,
    echo=settings.db_echo,
    pool_size=settings.db_pool_size,
    max_overflow=settings.db_max_overflow,
    pool_timeout=settings.db_pool_timeout,
    pool_recycle=settings.db_pool_recycle,
    pool_pre_ping=True,
)

async_session_maker = async_sessionmaker(
    bind=engine,
    class_=AsyncSession,
    expire_on_commit=False,
    autoflush=False,
    autocommit=False,
)


async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
    """
    Dependência que fornece sessão assíncrona.

    Faz commit em sucesso e rollback em exceções, garantindo consistência.
    """
    async with async_session_maker() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise


AsyncSessionDep = Annotated[AsyncSession, Depends(get_async_session)]


async def create_db_and_tables() -> None:
    """Cria tabelas apenas para desenvolvimento/testes."""
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)


async def dispose_engine() -> None:
    """Fecha conexões de forma apropriada."""
    await engine.dispose()

Application Factory e ciclo de vida (lifespan)

O padrão Application Factory cria a aplicação via função, melhorando testabilidade e permitindo múltiplas instâncias. O lifespan define eventos de startup e shutdown em um único contexto, substituindo ganchos antigos e dispersos. No startup, inicializações podem ocorrer, como aquecer caches e validar dependências. No shutdown, recursos como conexões e filas são encerrados de forma segura.

Em produção, é comum desabilitar documentação automática por segurança e redução de superfície de ataque. Middleware adiciona comportamento transversal, como compressão GZip e CORS. Rotas são agregadas por módulos e recebem prefixos de versão, facilitando evolução da API. Endpoints de health check permitem que orquestradores determinem se o serviço está vivo e pronto.

O exemplo a seguir combina lifespan, middlewares, roteadores e health checks. Ele usa structlog para logging estruturado, que registra campos em formato consistente. O readiness check executa uma query simples, validando conectividade com o banco. Esse conjunto forma uma base típica para operar em ambientes com balanceadores e réplicas.

from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager

import structlog
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from sqlalchemy import text

from src.core.config import get_settings
from src.core.database import AsyncSessionDep, dispose_engine
from src.auth.router import router as auth_router
from src.users.router import router as users_router

logger = structlog.get_logger()


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
    """
    Gerenciador de ciclo de vida da aplicação.

    Startup: inicializa recursos. Shutdown: limpa recursos apropriadamente.
    """
    settings = get_settings()

    logger.info(
        "Iniciando aplicacao",
        app_name=settings.app_name,
        environment=settings.environment,
    )

    yield

    logger.info("Encerrando aplicacao")
    await dispose_engine()


def create_app() -> FastAPI:
    """Fábrica da aplicação (Application Factory)."""
    settings = get_settings()

    app = FastAPI(
        title=settings.app_name,
        version=settings.app_version,
        debug=settings.debug,
        lifespan=lifespan,
        docs_url="/docs" if not settings.is_production else None,
        redoc_url="/redoc" if not settings.is_production else None,
        openapi_url="/openapi.json" if not settings.is_production else None,
    )

    app.add_middleware(GZipMiddleware, minimum_size=1000)

    if settings.backend_cors_origins:
        app.add_middleware(
            CORSMiddleware,
            allow_origins=[str(origin) for origin in settings.backend_cors_origins],
            allow_credentials=True,
            allow_methods=["*"],
            allow_headers=["*"],
        )

    app.include_router(
        auth_router,
        prefix=f"{settings.api_v1_prefix}/auth",
        tags=["Autenticacao"],
    )
    app.include_router(
        users_router,
        prefix=f"{settings.api_v1_prefix}/users",
        tags=["Usuarios"],
    )

    @app.get("/health", tags=["Health"])
    async def health_check() -> dict[str, str]:
        return {"status": "healthy", "environment": settings.environment}

    @app.get("/health/ready", tags=["Health"])
    async def readiness_check(session: AsyncSessionDep) -> dict[str, str]:
        await session.execute(text("SELECT 1"))
        return {"status": "ready"}

    return app


app = create_app()

Repository Pattern (camada de acesso a dados)

O Repository Pattern encapsula consultas e operações de persistência em uma classe dedicada. Isso reduz o acoplamento entre regras de negócio e detalhes do ORM, facilitando troca de estratégias e testes. Um repositório genérico oferece operações comuns como listar, contar e criar. Repositórios específicos adicionam consultas orientadas ao domínio, como buscar usuário por e-mail.

Em projetos grandes, essa separação diminui duplicação de SQL/ORM em rotas e serviços. Também melhora legibilidade, pois o serviço “fala” em termos de intenção, não em termos de query. A sessão é injetada e controlada por dependência, mantendo o repositório leve. Em aplicações assíncronas, métodos do repositório devem ser async para evitar bloqueio.

O código abaixo traz um repositório base genérico e um repositório específico de usuários. Ele demonstra CRUD essencial e consultas customizadas. O uso de typing com Generic e TypeVar traz segurança de tipos. Esse padrão tende a se manter estável mesmo quando o domínio cresce.

from collections.abc import Sequence
from typing import Generic, TypeVar
from uuid import UUID

from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from src.core.database import Base

ModelType = TypeVar("ModelType", bound=Base)


class BaseRepository(Generic[ModelType]):
    """Repositório genérico com operações CRUD comuns."""

    def __init__(self, model: type[ModelType], session: AsyncSession) -> None:
        self.model = model
        self.session = session

    async def get_by_id(self, id: UUID) -> ModelType | None:
        return await self.session.get(self.model, id)

    async def get_all(self, *, skip: int = 0, limit: int = 100) -> Sequence[ModelType]:
        stmt = select(self.model).offset(skip).limit(limit)
        result = await self.session.execute(stmt)
        return result.scalars().all()

    async def count(self) -> int:
        stmt = select(func.count()).select_from(self.model)
        result = await self.session.execute(stmt)
        return result.scalar_one()

    async def create(self, obj: ModelType) -> ModelType:
        self.session.add(obj)
        await self.session.flush()
        await self.session.refresh(obj)
        return obj

    async def delete(self, obj: ModelType) -> None:
        await self.session.delete(obj)
from collections.abc import Sequence

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from src.users.models import User
from src.users.repository import BaseRepository


class UserRepository(BaseRepository[User]):
    """Repositório de usuários com consultas específicas do domínio."""

    def __init__(self, session: AsyncSession) -> None:
        super().__init__(User, session)

    async def get_by_email(self, email: str) -> User | None:
        stmt = select(User).where(User.email == email)
        result = await self.session.execute(stmt)
        return result.scalar_one_or_none()

    async def get_active_users(self) -> Sequence[User]:
        stmt = select(User).where(User.is_active == True)
        result = await self.session.execute(stmt)
        return result.scalars().all()

Service Layer (regras de negócio e orquestração)

A Service Layer concentra regras de negócio e fluxos, evitando que endpoints virem um “lugar de tudo”. Nessa camada ficam validações de domínio, como unicidade de e-mail, e decisões como hashing de senha. Exceções específicas do domínio ajudam a diferenciar falhas esperadas de falhas inesperadas. Essa camada também coordena múltiplos repositórios quando necessário.

Quando a regra muda, o serviço é o lugar natural para alteração, preservando rotas simples. Essa separação aumenta testabilidade, pois o serviço pode ser testado sem HTTP. Além disso, a camada impede que detalhes de SQLAlchemy vazem para o nível de API. Em projetos com autenticação, costuma haver um service para auth e outro para usuários.

O exemplo abaixo mostra um serviço de usuários que consulta o repositório, valida regras e cria entidades. Ele usa uma função de hashing para armazenar apenas senha derivada, e não senha em texto. Exceções como “não encontrado” e “já existe” representam estados normais do domínio. Esse padrão mantém o controller fino e previsível.

from uuid import UUID

from src.core.security import get_password_hash
from src.users.exceptions import UserAlreadyExistsError, UserNotFoundError
from src.users.models import User
from src.users.repository import UserRepository
from src.users.schemas import UserCreate, UserUpdate


class UserService:
    """
    Regras de negócio de usuários.

    Valida regras, orquestra repositórios e levanta exceções do domínio.
    """

    def __init__(self, repository: UserRepository) -> None:
        self.repository = repository

    async def get_user(self, user_id: UUID) -> User:
        user = await self.repository.get_by_id(user_id)
        if not user:
            raise UserNotFoundError(user_id=user_id)
        return user

    async def get_user_by_email(self, email: str) -> User | None:
        return await self.repository.get_by_email(email)

    async def create_user(self, data: UserCreate) -> User:
        existing = await self.repository.get_by_email(data.email)
        if existing:
            raise UserAlreadyExistsError(email=data.email)

        user = User(
            email=data.email,
            hashed_password=get_password_hash(data.password),
            full_name=data.full_name,
        )
        return await self.repository.create(user)

    async def update_user(self, user_id: UUID, data: UserUpdate) -> User:
        user = await self.get_user(user_id)

        update_data = data.model_dump(exclude_unset=True)
        for field, value in update_data.items():
            setattr(user, field, value)

        return user

Injeção de dependências com Annotated e dependências reutilizáveis

A injeção de dependências no FastAPI resolve objetos automaticamente para rotas, como sessão de banco e serviços. O uso de Annotated torna a assinatura mais clara, combinando tipo e mecanismo de injeção em um único local. Dependências reutilizáveis reduzem repetição, como validar que um usuário existe antes de executar uma rota. Esse padrão também padroniza validações de parâmetros com Path.

Dependências por módulo ajudam a manter o domínio coeso e evitam importações cruzadas demais. Um provider cria o repositório a partir da sessão, e outro cria o serviço a partir do repositório. Aliases tipados como UserServiceDep deixam as rotas menores e mais legíveis. Em produção, esse desenho facilita observabilidade e testes, pois objetos são compostos de forma controlada.

O exemplo abaixo mostra providers, aliases e uma dependência que obtém o usuário ou gera erro. O nome “or_404” descreve intenção, mantendo consistência com APIs REST. Esse padrão mantém validação fora do endpoint e reduz duplicação. O trecho também demonstra descrição de parâmetro de rota.

from typing import Annotated
from uuid import UUID

from fastapi import Depends, Path

from src.core.database import AsyncSessionDep
from src.users.models import User
from src.users.repository import UserRepository
from src.users.service import UserService


def get_user_repository(session: AsyncSessionDep) -> UserRepository:
    return UserRepository(session)


def get_user_service(
    repository: Annotated[UserRepository, Depends(get_user_repository)],
) -> UserService:
    return UserService(repository)


UserRepositoryDep = Annotated[UserRepository, Depends(get_user_repository)]
UserServiceDep = Annotated[UserService, Depends(get_user_service)]


async def get_user_or_404(
    user_id: Annotated[UUID, Path(description="ID do usuário")],
    service: UserServiceDep,
) -> User:
    """Garante que o usuário existe; caso contrário, a camada de serviço falha."""
    return await service.get_user(user_id)


CurrentUserDep = Annotated[User, Depends(get_user_or_404)]

Router fino (controller HTTP) e contratos com schemas

Um router deve ser um controller fino: recebe dados HTTP, chama o service e devolve resposta. Isso reduz o risco de rotas virarem um lugar de regras de negócio e queries, o que piora manutenção. Schemas (modelos Pydantic) funcionam como DTOs, definindo entrada e saída com validação. Essa separação padroniza contratos e torna respostas previsíveis para clientes.

Paginação com skip e limit é uma estratégia simples e comum para listas. Códigos HTTP como 201 em criação tornam a API consistente. Em endpoints que retornam modelos, o uso de model_validate converte entidades do ORM para schemas de saída. Esse padrão cria uma fronteira clara entre domínio e HTTP.

O trecho a seguir mostra um router de usuários com listagem, criação, busca e atualização. Ele injeta o serviço via dependência e evita lógica de negócio no endpoint. O endpoint de busca utiliza a dependência CurrentUserDep para garantir existência antes de responder. Essa abordagem reduz repetição e padroniza comportamento.

from uuid import UUID

from fastapi import APIRouter, status

from src.users.dependencies import CurrentUserDep, UserServiceDep
from src.users.schemas import UserCreate, UserRead, UserUpdate

router = APIRouter()


@router.get("", response_model=list[UserRead])
async def list_users(
    service: UserServiceDep,
    skip: int = 0,
    limit: int = 100,
) -> list[UserRead]:
    users = await service.repository.get_all(skip=skip, limit=limit)
    return [UserRead.model_validate(u) for u in users]


@router.post("", response_model=UserRead, status_code=status.HTTP_201_CREATED)
async def create_user(
    data: UserCreate,
    service: UserServiceDep,
) -> UserRead:
    user = await service.create_user(data)
    return UserRead.model_validate(user)


@router.get("/{user_id}", response_model=UserRead)
async def get_user(user: CurrentUserDep) -> UserRead:
    return UserRead.model_validate(user)


@router.patch("/{user_id}", response_model=UserRead)
async def update_user(
    user_id: UUID,
    data: UserUpdate,
    service: UserServiceDep,
) -> UserRead:
    user = await service.update_user(user_id, data)
    return UserRead.model_validate(user)

Dockerfile de produção com build em múltiplas etapas e UV

Um Dockerfile de produção deve gerar imagem pequena, segura e reprodutível. O build em múltiplas etapas separa a fase de instalação (builder) da fase final (runtime), copiando apenas o necessário. O uso de usuário non-root reduz impacto de exploração de vulnerabilidades. O UV acelera instalação e respeita o lockfile, aumentando consistência entre builds.

Variáveis como UV_COMPILE_BYTECODE ajudam a pré-compilar bytecode para melhorar tempo de startup. O uso de cache mount para UV reduz tempo de build em pipelines. A fase runtime recebe apenas o ambiente virtual e o código, diminuindo superfície e tamanho. Em APIs, o servidor uvicorn com múltiplos workers aumenta throughput em máquinas multicore.

O exemplo abaixo apresenta um Dockerfile completo com UV, builder e runtime. Ele inclui health check interno para facilitar orquestração por plataformas. O comando final ativa uvloop e httptools para melhor performance de event loop e parsing HTTP. Esse conjunto é típico em ambientes de produção com tráfego relevante.

# syntax=docker/dockerfile:1.9

FROM python:3.12-slim-bookworm AS builder

COPY --from=ghcr.io/astral-sh/uv:0.5.4 /uv /usr/local/bin/uv

WORKDIR /app

ENV UV_COMPILE_BYTECODE=1 \
    UV_LINK_MODE=copy \
    UV_PYTHON_DOWNLOADS=never \
    UV_PROJECT_ENVIRONMENT=/app/.venv

RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv sync --frozen --no-install-project --no-dev

COPY src ./src
COPY alembic ./alembic
COPY alembic.ini pyproject.toml ./

RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-dev --no-editable


FROM python:3.12-slim-bookworm AS runtime

RUN groupadd -g 1001 app && \
    useradd -u 1001 -g app -m -d /app -s /bin/false app

WORKDIR /app

ENV PATH="/app/.venv/bin:$PATH" \
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONFAULTHANDLER=1

COPY --from=builder --chown=app:app /app/.venv /app/.venv
COPY --from=builder --chown=app:app /app/src /app/src
COPY --from=builder --chown=app:app /app/alembic /app/alembic
COPY --from=builder --chown=app:app /app/alembic.ini /app/alembic.ini

USER app

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
    CMD python -c "import httpx; httpx.get('http://localhost:8000/health', timeout=5)" || exit 1

CMD ["uvicorn", "src.main:app", \
     "--host", "0.0.0.0", \
     "--port", "8000", \
     "--workers", "4", \
     "--loop", "uvloop", \
     "--http", "httptools"]

Docker Compose para desenvolvimento e produção

Docker Compose orquestra múltiplos serviços, como API e PostgreSQL, em uma rede comum. Em desenvolvimento, volumes montados permitem editar código e recarregar o servidor com rapidez. Dependências como o banco podem ter health check, garantindo que a API suba apenas quando o banco estiver pronto. Em produção, configurações mudam, priorizando restart automático, réplicas e limites de recursos.

Em ambientes produtivos, é comum adicionar um reverse proxy como Nginx para TLS/HTTPS e roteamento. A API pode ser exposta internamente e o proxy publica portas para a internet. Variáveis de ambiente são carregadas por arquivo, mantendo segredos fora do código. O uso de networks separadas reduz exposição direta do banco.

A seguir aparecem dois exemplos de compose: um para desenvolvimento e outro para produção. O de desenvolvimento usa reload e volumes, enquanto o de produção define réplica, health check e proxy. Essa separação evita que configurações de dev vazem para o ambiente produtivo. O conteúdo demonstra um padrão realista e amplamente utilizado.

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
      target: builder
    command: uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload
    ports:
      - "8000:8000"
    volumes:
      - ./src:/app/src:cached
      - ./tests:/app/tests:cached
      - ./alembic:/app/alembic:cached
      - /app/.venv
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
    networks:
      - app-network

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-dev}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-devpassword}
      POSTGRES_DB: ${POSTGRES_DB:-devdb}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-dev}"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - app-network

volumes:
  postgres_data:

networks:
  app-network:
    driver: bridge
services:
  api:
    image: ${DOCKER_REGISTRY}/my-app:${VERSION:-latest}
    restart: unless-stopped
    deploy:
      replicas: 2
      resources:
        limits:
          cpus: "1"
          memory: 1G
        reservations:
          cpus: "0.5"
          memory: 512M
    environment:
      - ENVIRONMENT=production
    env_file:
      - .env.production
    expose:
      - "8000"
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    networks:
      - internal

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - api
    networks:
      - internal

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    volumes:
      - postgres_data_prod:/var/lib/postgresql/data
    env_file:
      - .env.production
    healthcheck:
      test: ["CMD-SHELL", "pg_isready"]
      interval: 30s
      timeout: 10s
    networks:
      - internal

volumes:
  postgres_data_prod:

networks:
  internal:
    driver: bridge

Arquivos essenciais de repositório: .dockerignore e pre-commit

O arquivo .dockerignore evita copiar itens desnecessários para o contexto do build, acelerando builds e reduzindo tamanho final. Pastas como .git, caches, venv e artefatos de teste devem ser ignorados. Em projetos Python, ignorar __pycache__ e caches de linters reduz ruído. Esse arquivo é crítico quando o pipeline constrói imagens com frequência.

O pre-commit executa verificações antes de commits, padronizando formatação e evitando problemas óbvios. Hooks comuns incluem formatador, linter e verificações de YAML e conflitos de merge. Em times, isso reduz divergência de estilo e diminui retrabalho em revisões. A configuração costuma fixar versões de hooks para manter consistência.

Os exemplos abaixo mostram um .dockerignore típico e um arquivo de pre-commit com Ruff. Eles cobrem caches comuns e evitam vazar arquivos sensíveis como .env. Também impedem que arquivos grandes ou chaves privadas sejam acidentalmente versionados. Esse conjunto fortalece a higiene do repositório em produção.

**/.git
**/.gitignore
**/.venv
**/__pycache__
**/*.pyc
**/*.pyo
**/*.egg-info
**/.pytest_cache
**/.mypy_cache
**/.ruff_cache
**/.coverage
**/htmlcov
**/dist
**/build
**/*.md
!README.md
**/docker-compose*.yml
**/Dockerfile*
**/.env*
!.env.example
**/tests
**/.vscode
**/.idea
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.8.0
    hooks:
      - id: ruff
        args: [--fix, --exit-non-zero-on-fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
      - id: check-merge-conflict
      - id: detect-private-key

Boas práticas de segurança em produção

Segurança em produção começa por reduzir privilégios e evitar segredos no código. Rodar containers como non-root limita ações em caso de comprometimento do processo. Segredos devem vir de variáveis de ambiente e arquivos de configuração fora do repositório, nunca hardcoded. Desabilitar documentação e OpenAPI em produção reduz exposição de rotas e modelos.

Health checks são parte de segurança operacional, pois permitem remover instâncias do tráfego quando estão degradadas. Um reverse proxy habilita HTTPS e centraliza políticas como rate limiting, protegendo a API de abuso. O lockfile e verificação de hashes fortalecem a cadeia de suprimentos, reduzindo risco de dependências adulteradas. Somado a isso, logs em stdout e métricas coerentes aumentam capacidade de auditoria.

Práticas comuns incluem controle de CORS com lista explícita de origens e headers. Tokens de autenticação, como JWT, exigem chave forte e rotação planejada. Hash de senha deve usar algoritmo apropriado, como bcrypt, e nunca armazenar senha em texto. Esse conjunto reduz vulnerabilidades típicas em APIs públicas.

Otimizações de performance e estabilidade

Performance em APIs Python depende mais de I/O eficiente e configuração correta do que de micro-otimizações. uvloop substitui o event loop padrão por uma implementação mais rápida, melhorando throughput em workloads assíncronos. httptools acelera parsing HTTP no uvicorn, reduzindo overhead por requisição. Múltiplos workers aproveitam múltiplos núcleos, isolando falhas e aumentando capacidade de resposta.

No banco, pool_pre_ping reduz erros por conexões quebradas, comuns em ambientes com NAT e balanceadores. Parâmetros como pool_size e max_overflow controlam concorrência e evitam exaustão de conexões no PostgreSQL. O flag expire_on_commit=False evita recarregamentos desnecessários e problemas ao acessar entidades após commit. O build multi-stage e o cache de dependências aceleram deploys e reduzem tamanho de imagem.

Otimização também envolve previsibilidade: builds reprodutíveis evitam “funciona em um lugar e falha em outro”. Fixar versões via uv.lock reduz diferenças entre réplicas. Health checks de readiness evitam enviar tráfego antes de dependências estarem prontas. Esse conjunto melhora estabilidade e reduz incidentes em picos de carga.

Encerramento: um padrão coerente para APIs Python em produção (2025)

A combinação de FastAPI, Docker e UV forma uma base consistente para serviços de alta demanda quando acompanhada de organização por camadas. Configuração validada com Pydantic Settings, banco assíncrono com SQLAlchemy 2.0 e separação Router–Service–Repository reduzem acoplamento e melhoram testabilidade. O ciclo de vida via lifespan e health checks reforça previsibilidade operacional em orquestração.

O empacotamento com Docker multi-stage, usuário non-root, dependências travadas e automações como pre-commit elevam qualidade e segurança. A adoção de uvloop, httptools e parâmetros corretos de pool melhora desempenho e reduz falhas intermitentes. Esses padrões, combinados, entregam um projeto autossuficiente e pronto para crescer com clareza e estabilidade, do desenvolvimento ao ambiente produtivo.