Por que um blueprint Axum + PostgreSQL ajuda mais do que um deploy genérico
Muitos guias de deploy Rust param no ponto em que o binário responde 200 OK. Isso é útil, mas incompleto para quem está construindo uma API real. Uma aplicação Axum em produção normalmente precisa de banco PostgreSQL, migrations, variáveis de ambiente, logs estruturados, endpoint de saúde, backup, rollback e um jeito simples de subir tudo sem depender de Kubernetes no primeiro dia.
Para produtos pequenos, portfólios profissionais, APIs internas e MVPs brasileiros, Docker Compose com PostgreSQL em uma VPS continua sendo um caminho muito competitivo. Ele é mais explícito do que uma plataforma mágica, mais barato do que um cluster gerenciado e simples o bastante para uma pessoa operar. Ao mesmo tempo, força boas práticas que aparecem em vagas Rust: containerização, banco relacional, observabilidade, deploy reproduzível e disciplina operacional.
Este guia é um blueprint prático. Ele não tenta substituir uma arquitetura enterprise, mas entrega uma base segura para publicar uma API Axum com SQLx e PostgreSQL usando Compose. Se você ainda está montando a imagem Docker, leia também Rust e Docker para builds de produção. Se quer uma visão mais ampla de VPS, Nginx e systemd, veja Deploy Rust em VPS.
Arquitetura de produção mínima
A topologia recomendada é simples:
Internet
|
v
Nginx / TLS
|
v
docker compose network
|-- axum-api :3000
|-- postgres :5432
Nginx fica na borda, termina TLS e encaminha tráfego para o container axum-api. O PostgreSQL não expõe porta pública. A API acessa o banco pela rede interna do Compose usando o hostname postgres. Essa separação reduz a superfície de ataque e mantém o deploy fácil de entender.
Para uma primeira versão, rode tudo em uma única VPS com disco persistente e backup externo. Quando o produto crescer, você pode mover o banco para um PostgreSQL gerenciado, adicionar Redis, separar workers ou migrar para Kubernetes. O importante é não começar com complexidade que você ainda não consegue operar.
Aplicação Axum pronta para container
Uma API Axum precisa escutar em 0.0.0.0, carregar configuração por ambiente e expor um health check barato. Um esqueleto típico:
use axum::{extract::State, routing::get, Router};
use sqlx::PgPool;
use std::net::SocketAddr;
#[derive(Clone)]
struct AppState {
db: PgPool,
}
async fn healthz() -> &'static str {
"ok"
}
async fn readyz(State(state): State<AppState>) -> Result<&'static str, String> {
sqlx::query_scalar::<_, i32>("select 1")
.fetch_one(&state.db)
.await
.map_err(|err| err.to_string())?;
Ok("ready")
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.json()
.with_env_filter(std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()))
.init();
let database_url = std::env::var("DATABASE_URL")?;
let db = PgPool::connect(&database_url).await?;
let state = AppState { db };
let app = Router::new()
.route("/healthz", get(healthz))
.route("/readyz", get(readyz))
.with_state(state);
let addr: SocketAddr = "0.0.0.0:3000".parse()?;
let listener = tokio::net::TcpListener::bind(addr).await?;
tracing::info!(%addr, "api axum iniciada");
axum::serve(listener, app).await?;
Ok(())
}
Separe /healthz de /readyz. O primeiro responde se o processo está vivo. O segundo verifica dependências, como PostgreSQL. Isso evita derrubar a aplicação inteira por uma oscilação curta do banco e ajuda a depurar incidentes sem adivinhar.
Dockerfile para a API
Um Dockerfile multi-stage mantém a imagem final pequena e sem toolchain de compilação:
FROM rust:1.88-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
pkg-config libssl-dev ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY Cargo.toml Cargo.lock ./
COPY migrations ./migrations
COPY src ./src
RUN cargo build --release
FROM debian:bookworm-slim AS runtime
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
RUN useradd -r -u 10001 appuser
COPY --from=builder /app/target/release/minha-api /usr/local/bin/minha-api
USER appuser
EXPOSE 3000
ENV RUST_LOG=info
CMD ["/usr/local/bin/minha-api"]
Em projetos maiores, adicione cargo-chef para cache de dependências. Em projetos com OpenSSL, confira se as bibliotecas necessárias estão presentes na imagem final. Se preferir reduzir ainda mais a imagem, você pode estudar builds com musl, mas não comece por isso se ainda está validando produto.
docker-compose.yml com PostgreSQL
Um docker-compose.yml de produção pode ficar assim:
services:
axum-api:
image: registry.example.com/minha-api:2026-05-21
container_name: axum-api
restart: unless-stopped
env_file:
- /etc/minha-api/api.env
depends_on:
postgres:
condition: service_healthy
ports:
- "127.0.0.1:3000:3000"
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3000/healthz"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
postgres:
image: postgres:16-alpine
container_name: axum-postgres
restart: unless-stopped
env_file:
- /etc/minha-api/postgres.env
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
No arquivo /etc/minha-api/api.env, use a URL interna do Compose:
DATABASE_URL=postgres://minha_api:senha-forte@postgres:5432/minha_api
RUST_LOG=info,tower_http=info,sqlx=warn
No /etc/minha-api/postgres.env:
POSTGRES_DB=minha_api
POSTGRES_USER=minha_api
POSTGRES_PASSWORD=senha-forte
Não commite esses arquivos. Em produção, eles pertencem ao servidor ou a um gerenciador de secrets. Se você usa CI/CD, injete variáveis no momento do deploy, não no repositório.
Migrations: rode antes de liberar tráfego
Com SQLx, migrations devem ser parte do processo de deploy, não uma tarefa manual esquecida. O fluxo mais seguro é:
- Subir uma imagem nova sem remover a anterior.
- Rodar
sqlx migrate runcontra o banco de produção. - Reiniciar a API apontando para a nova imagem.
- Conferir
/readyz, logs e métricas. - Manter a tag anterior disponível para rollback.
Você pode empacotar o binário sqlx-cli em uma imagem separada, rodar migrations a partir do CI ou usar um comando temporário no servidor. O ponto crítico é versionar cada alteração de schema e evitar mudanças irreversíveis sem plano. Para deploys pequenos, uma regra prática funciona bem: primeiro adicione colunas/tabelas compatíveis, publique a aplicação que usa o novo schema e só depois remova campos antigos em outro deploy.
Nginx na frente da API
Mesmo quando Compose publica a porta só em 127.0.0.1, coloque Nginx ou Caddy na frente para TLS, compressão e cabeçalhos. Um bloco Nginx mínimo:
server {
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
}
}
Configure TLS com Let’s Encrypt, limite tamanho de payload quando necessário e adicione rate limiting se a API for pública. Em Axum, leia X-Forwarded-Proto com cuidado se precisar gerar URLs absolutas. Para autenticação, prefira tokens curtos, rotação clara e logs que não imprimam secrets.
Logs, backup e rollback
O deploy só está completo quando você consegue responder três perguntas: o que está rodando, se está saudável e como voltar atrás. Use tags imutáveis de imagem, como SHA do commit, em vez de latest. Registre o commit em uma variável APP_VERSION ou endpoint /version. Mantenha logs em JSON com Tracing e consulte com docker logs axum-api ou agregue em Loki, Vector ou outra stack.
Para PostgreSQL, configure backup antes do primeiro usuário real. Um pg_dump diário para armazenamento externo já é melhor do que nada. Teste restore em outro diretório ou outra máquina. Backup que nunca foi restaurado é esperança, não estratégia.
Rollback deve ser simples: trocar a tag da imagem no Compose e rodar docker compose up -d. O cuidado está no banco. Se a migration removeu coluna ou transformou dados de forma irreversível, rollback da aplicação pode falhar. Por isso migrations compatíveis e pequenas são parte do design, não burocracia.
Checklist antes de publicar
Antes de apontar tráfego real para a API, revise:
DATABASE_URLnão aparece no repositório nem nos logs;- PostgreSQL não expõe
5432para a internet; /healthze/readyzrespondem como esperado;- migrations rodam em ambiente limpo;
- imagem Docker usa usuário não-root;
- Nginx termina TLS e encaminha cabeçalhos corretos;
- existe backup automático e restore testado;
- a tag anterior da imagem ainda está disponível;
- logs têm request id, status, latência e erro sem dados sensíveis.
Esse checklist transforma um projeto de portfólio em algo muito mais próximo de produção. Para carreira, isso importa. Empresas brasileiras que usam Rust em backend, fintech, infraestrutura ou ferramentas internas querem ver que você entende o ciclo completo: código, banco, deploy, operação e manutenção. Combine este guia com o tutorial de Rust com PostgreSQL, a página de Cargo, o guia de CI/CD para Rust e a lista de empresas que usam Rust para montar um portfólio forte.
Se você vem de Go e quer comparar a mesma mentalidade operacional em outra linguagem, o ecossistema do Go Brasil também cobre backend, Docker e serviços em produção. A diferença é que Rust força mais decisões em tipos e em tempo de compilação, o que combina muito bem com APIs que precisam ser rápidas, previsíveis e seguras.