Shuttle é uma plataforma de deploy criada para aplicações Rust: você declara o servidor, PostgreSQL e secrets na integração do projeto, testa com cargo shuttle run e publica com cargo shuttle deploy. Para uma API Axum, isso reduz o trabalho inicial com Dockerfile, proxy reverso, provisionamento e scripts de servidor. É uma boa escolha para protótipos, APIs internas, projetos de portfólio e produtos pequenos que querem chegar rapidamente a uma URL pública sem esconder o código Rust atrás de uma configuração extensa de infraestrutura.
O fluxo essencial é:
cargo install cargo-shuttle --locked
cargo shuttle login
cargo shuttle init
cargo shuttle run
cargo shuttle deploy
Os comandos e recursos disponíveis podem evoluir. Antes de automatizar a CI, confirme a interface da versão instalada com cargo shuttle --version e cargo shuttle --help, consulte a documentação oficial da plataforma e fixe uma versão homologada para o time.
Resposta rápida: quando usar Shuttle
| Cenário | Shuttle é uma boa escolha? | Por quê |
|---|---|---|
| API Axum de portfólio | Sim | entrega uma URL pública sem exigir uma stack completa de DevOps |
| MVP com PostgreSQL | Sim | integra aplicação e banco com pouco boilerplate operacional |
| Serviço interno pequeno | Sim | reduz manutenção de VPS e scripts de deploy |
| Aprender containers e Linux | Não como única opção | Shuttle abstrai justamente parte do que você quer estudar |
| Vários serviços auxiliares personalizados | Depende | valide se todos os componentes cabem nos recursos suportados |
| Rede privada complexa ou compliance rígido | Provavelmente não | containers ou infraestrutura dedicada oferecem mais controle |
| Produto que precisa trocar de provedor facilmente | Depende | preserve limites arquiteturais para não acoplar todo o domínio à plataforma |
A decisão prática é simples: use Shuttle quando a velocidade para publicar uma aplicação Rust vale mais do que controlar cada camada da infraestrutura. Escolha Docker, VPS ou Kubernetes quando rede, runtime, observabilidade, compliance ou portabilidade forem requisitos centrais desde o início.
Como Shuttle se encaixa em um backend Rust
Em um deploy tradicional, uma API pode exigir:
- compilar o binário em uma imagem multi-stage;
- provisionar uma máquina ou serviço de containers;
- configurar TLS e proxy reverso;
- criar PostgreSQL e credenciais;
- injetar secrets;
- configurar logs e health checks;
- montar um pipeline de atualização e rollback.
Esse controle é útil, mas também cria uma fila de tarefas antes de o primeiro endpoint chegar ao usuário. Shuttle oferece uma experiência mais integrada ao ecossistema Rust. O ponto de entrada deixa de apenas iniciar um servidor em uma porta arbitrária e passa a devolver o serviço que a plataforma deve executar.
Com Axum, o formato conceitual é este:
use axum::{routing::get, Router};
use shuttle_axum::ShuttleAxum;
async fn health() -> &'static str {
"ok"
}
#[shuttle_runtime::main]
async fn main() -> ShuttleAxum {
let app = Router::new()
.route("/health", get(health));
Ok(app.into())
}
A aplicação continua sendo Axum. Rotas, extractors, middlewares, estado, testes e regras de domínio permanecem no código normal. A integração com Shuttle deve ficar concentrada na composição da infraestrutura e no ponto de entrada, evitando que handlers de negócio conheçam detalhes do provedor.
Se você ainda não domina o framework, comece pelo guia de Axum e pelo tutorial de API REST com Axum. Shuttle simplifica o deploy, mas não corrige modelagem ruim, handlers acoplados ou ausência de testes.
Criando o projeto
Você pode iniciar um projeto pela CLI ou adaptar uma API existente. Para explorar as opções disponíveis na versão instalada:
cargo shuttle init --help
Ao escolher um template Axum, revise os arquivos gerados antes de continuar. Templates aceleram o começo, mas nomes de crates, features e estrutura podem mudar entre versões. O repositório deve continuar compreensível sem depender de um gerador.
Uma estrutura pequena e sustentável seria:
minha-api/
├── Cargo.toml
├── Secrets.toml.example
├── migrations/
│ └── 202609060001_create_tasks.sql
├── src/
│ ├── main.rs
│ ├── app.rs
│ ├── config.rs
│ ├── error.rs
│ └── routes/
│ ├── mod.rs
│ └── tasks.rs
└── tests/
└── api.rs
Não coloque toda a aplicação em main.rs. Deixe o arquivo responsável por receber recursos da plataforma e montar o estado. Assim, os testes podem construir o mesmo Router sem subir uma implantação real.
Dependências para Axum, Shuttle e SQLx
Use cargo add para selecionar versões compatíveis no momento em que o projeto for criado:
cargo add axum
cargo add shuttle-runtime
cargo add shuttle-axum
cargo add shuttle-shared-db --features postgres,sqlx
cargo add sqlx --features runtime-tokio-rustls,postgres,migrate,uuid,chrono
cargo add serde --features derive
cargo add tokio --features macros,rt-multi-thread
cargo add tracing tracing-subscriber
cargo add thiserror
Depois, inspecione a árvore:
cargo tree -d
cargo check
Evite copiar números de versão de um artigo sem verificar a compatibilidade atual. A combinação entre shuttle-runtime, shuttle-axum, shuttle-shared-db, Axum e SQLx é parte do contrato de build. Versione o Cargo.lock da aplicação e deixe atualizações para pull requests explícitos.
O guia de cargo tree ajuda a localizar versões duplicadas e features inesperadas. Para controlar a versão mínima do compilador, veja cargo-msrv e MSRV na CI.
Aplicação Axum com PostgreSQL gerenciado
Uma integração típica recebe um PgPool no ponto de entrada e o coloca no estado do Axum:
use axum::{
extract::State,
http::StatusCode,
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use shuttle_axum::ShuttleAxum;
use sqlx::{FromRow, PgPool};
#[derive(Clone)]
struct AppState {
db: PgPool,
}
#[derive(Debug, Serialize, FromRow)]
struct Task {
id: i64,
title: String,
done: bool,
}
#[derive(Debug, Deserialize)]
struct NewTask {
title: String,
}
async fn health() -> &'static str {
"ok"
}
async fn list_tasks(
State(state): State<AppState>,
) -> Result<Json<Vec<Task>>, (StatusCode, String)> {
let tasks = sqlx::query_as::<_, Task>(
"select id, title, done from tasks order by id desc limit 100",
)
.fetch_all(&state.db)
.await
.map_err(internal_error)?;
Ok(Json(tasks))
}
async fn create_task(
State(state): State<AppState>,
Json(input): Json<NewTask>,
) -> Result<(StatusCode, Json<Task>), (StatusCode, String)> {
let title = input.title.trim();
if title.is_empty() || title.len() > 160 {
return Err((
StatusCode::BAD_REQUEST,
"title deve conter entre 1 e 160 caracteres".into(),
));
}
let task = sqlx::query_as::<_, Task>(
r#"
insert into tasks (title)
values ($1)
returning id, title, done
"#,
)
.bind(title)
.fetch_one(&state.db)
.await
.map_err(internal_error)?;
Ok((StatusCode::CREATED, Json(task)))
}
fn internal_error(error: sqlx::Error) -> (StatusCode, String) {
tracing::error!(%error, "database operation failed");
(
StatusCode::INTERNAL_SERVER_ERROR,
"erro interno".into(),
)
}
#[shuttle_runtime::main]
async fn main(
#[shuttle_shared_db::Postgres] db: PgPool,
) -> ShuttleAxum {
sqlx::migrate!()
.run(&db)
.await
.map_err(shuttle_runtime::CustomError::new)?;
let state = AppState { db };
let app = Router::new()
.route("/health", get(health))
.route("/tasks", get(list_tasks).post(create_task))
.with_state(state);
Ok(app.into())
}
A assinatura exata dos atributos e tipos deve ser conferida na documentação da versão usada. O desenho arquitetural, porém, é estável: a plataforma fornece o recurso, main monta o estado e os handlers recebem dependências por State.
A migration correspondente:
create table if not exists tasks (
id bigserial primary key,
title varchar(160) not null,
done boolean not null default false,
created_at timestamptz not null default now()
);
Para aplicações reais, não deixe erros SQL chegarem ao cliente. Registre detalhes internamente e devolva uma mensagem estável. Também não faça queries sem limite em coleções que podem crescer.
Migrations: conveniência com responsabilidade
Executar sqlx::migrate!() na inicialização é conveniente para projetos pequenos. O padrão garante que uma instância nova encontre o schema esperado, mas exige cuidado quando a aplicação cresce.
Uma migration segura deve ser:
- versionada no mesmo repositório;
- compatível com a versão anterior da aplicação durante o deploy;
- rápida ou executada em uma etapa separada;
- revisada para locks em tabelas grandes;
- acompanhada por estratégia de rollback lógico;
- testada contra um banco com dados representativos.
Evite remover uma coluna no mesmo deploy em que o código deixa de usá-la. Prefira o padrão expandir–migrar–contrair: adicione a nova estrutura, publique código compatível, migre dados e só depois remova o legado.
Para um fluxo completo com SQLx, leia migrations PostgreSQL com SQLx. O fato de a infraestrutura ser gerenciada não elimina riscos de schema.
Secrets sem vazar credenciais
Chaves de API, tokens e senhas não pertencem ao Git. Durante o desenvolvimento local, Shuttle trabalha com um mecanismo de secrets que pode ser injetado no ponto de entrada. Um exemplo conceitual:
use shuttle_runtime::SecretStore;
#[shuttle_runtime::main]
async fn main(
#[shuttle_runtime::Secrets] secrets: SecretStore,
) -> ShuttleAxum {
let api_key = secrets
.get("EXTERNAL_API_KEY")
.ok_or_else(|| shuttle_runtime::CustomError::new(
std::io::Error::other("EXTERNAL_API_KEY ausente"),
))?;
// Monte clientes externos sem registrar api_key nos logs.
// ...
}
Mantenha apenas um arquivo de exemplo:
# Secrets.toml.example
EXTERNAL_API_KEY = "substitua-localmente"
E ignore o arquivo real:
Secrets.toml
Antes de publicar, confirme no help da CLI como cadastrar e atualizar secrets remotos na versão atual. Nunca passe um segredo diretamente na linha de comando se ele puder terminar no histórico do shell, em logs de CI ou na lista de processos.
Também evite imprimir a configuração completa no startup. Logs como tracing::info!(?config) podem expor tokens aninhados mesmo quando a intenção era diagnosticar apenas host e timeout.
Rodando localmente
O ciclo local começa com:
cargo fmt --all -- --check
cargo check
cargo test
cargo shuttle run
A CLI inicia a aplicação e os recursos locais necessários ou configurados para desenvolvimento. Teste o endpoint em outro terminal:
curl -i http://127.0.0.1:8000/health
curl -i \
-H 'content-type: application/json' \
-d '{"title":"Publicar API Rust"}' \
http://127.0.0.1:8000/tasks
curl -i http://127.0.0.1:8000/tasks
A porta local pode variar conforme configuração e versão; use a URL mostrada pela CLI. Não codifique a porta do ambiente de produção dentro da aplicação. A plataforma controla como o serviço recebe tráfego.
Além do teste manual, construa o Router em uma função independente:
fn app(state: AppState) -> Router {
Router::new()
.route("/health", get(health))
.route("/tasks", get(list_tasks).post(create_task))
.with_state(state)
}
Isso permite testar requests com tower::ServiceExt sem abrir socket. Testes rápidos no processo detectam regressões antes do deploy e não dependem da disponibilidade da plataforma.
Fazendo o primeiro deploy
Depois de autenticar e validar localmente:
cargo shuttle deploy
Acompanhe a saída da CLI e confirme que o build terminou, o serviço iniciou e a URL responde. Não trate “comando terminou sem erro” como validação completa.
Faça um smoke test:
curl -fsS https://SEU-PROJETO.example/health
Depois verifique:
- resposta do endpoint de leitura;
- escrita e leitura no PostgreSQL;
- rejeição de payload inválido;
- headers de segurança necessários;
- logs sem secrets ou dados pessoais;
- comportamento depois de reiniciar a aplicação;
- latência após período sem tráfego, se o plano puder suspender recursos;
- limites e quotas atuais da conta.
Planos, regiões, domínios, quotas e política de suspensão são informações voláteis. Consulte o painel e a documentação oficial antes de prometer custo, disponibilidade ou latência a clientes.
Logs, tracing e diagnóstico
Use tracing em vez de espalhar println! pela aplicação. Registre eventos com campos úteis:
tracing::info!(task_id = task.id, "task created");
tracing::warn!(user_id = %user_id, "rate limit reached");
tracing::error!(error = %error, operation = "list_tasks", "query failed");
Não registre:
- tokens de autorização;
- cookies de sessão;
- secrets;
- payloads completos com dados pessoais;
- strings de conexão;
- senhas ou hashes que não precisam aparecer;
- corpo integral de respostas externas.
Confira os comandos atuais de logs com:
cargo shuttle --help
Em um incidente, responda nesta ordem:
- o processo iniciou?
- a rota
/healthresponde? - o banco está acessível?
- a migration terminou?
- o secret obrigatório existe?
- o erro ocorre em toda request ou em um endpoint?
- houve mudança de dependência, schema ou configuração?
O guia de Tracing em Rust aprofunda spans, campos estruturados e integração com aplicações assíncronas.
Domínio, CORS e frontend
Se um frontend em outro domínio chama a API, configure CORS com uma allowlist explícita. Não use Access-Control-Allow-Origin: * junto de cookies ou credenciais.
Com tower-http, a ideia é:
use axum::http::{HeaderValue, Method};
use tower_http::cors::CorsLayer;
let cors = CorsLayer::new()
.allow_origin("https://app.exemplo.com".parse::<HeaderValue>()?)
.allow_methods([Method::GET, Method::POST]);
let app = app(state).layer(cors);
Valide a API primeiro no endereço fornecido pela plataforma. Depois configure domínio personalizado seguindo o procedimento atual de DNS e TLS documentado pelo Shuttle. Não altere DNS antes de saber como voltar para o endpoint anterior.
Se o cliente for uma SPA Rust/WebAssembly, o guia de Trunk para build e deploy cobre a parte estática. Mantenha frontend e backend como artefatos independentes quando isso simplificar cache, rollback e operação.
CI/CD sem transformar deploy em caixa-preta
Antes de automatizar a publicação, faça pelo menos um deploy manual e documente o que a CLI precisa. Uma pipeline mínima deve separar validação de deploy:
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo shuttle deploy
Na CI:
- fixe a versão de
cargo-shuttle; - guarde o token de autenticação no cofre do provedor;
- limite o deploy à branch protegida;
- use concorrência para evitar duas publicações simultâneas;
- não imprima variáveis secretas;
- faça um smoke test depois do deploy;
- preserve logs suficientes para identificar commit e versão;
- defina como reverter para a versão anterior.
Se o projeto existe simultaneamente no GitHub e no Gitea, não copie uma alteração de workflow para apenas um lado por hábito. Compare triggers, versões de Rust, instalação da CLI, nomes de secrets e branch de deploy para evitar duas definições incompatíveis de “produção”.
Shuttle vs Docker, VPS e serverless genérico
| Opção | Vantagem principal | Custo operacional | Melhor para |
|---|---|---|---|
| Shuttle | experiência integrada a Rust | baixo no início | APIs Rust pequenas, MVPs e portfólio |
| Docker em VPS | controle e portabilidade | médio | serviços com componentes conhecidos e orçamento previsível |
| Container gerenciado | equilíbrio entre abstração e controle | médio | times que já padronizam imagens OCI |
| Kubernetes | orquestração e políticas avançadas | alto | múltiplos serviços, plataforma interna e escala organizacional |
| Serverless genérico | escala por request e integrações do provedor | variável | funções curtas e workloads compatíveis com o runtime |
Shuttle não precisa vencer todas as comparações. Seu valor é reduzir a distância entre cargo test e uma aplicação Rust pública. Quando o produto exigir uma topologia que a plataforma não representa bem, migre com intenção.
Para preservar portabilidade:
- mantenha regras de negócio em crates independentes;
- injete banco e clientes por estado ou traits;
- não use tipos da plataforma dentro do domínio;
- aplique migrations SQL padrão;
- mantenha configuração explícita;
- teste o
Routerfora do runtime de deploy; - registre uma estratégia alternativa de execução do binário.
O tutorial de deploy Axum com Docker Compose e PostgreSQL mostra o caminho com mais controle. Já deploy Rust em VPS com Docker e systemd ajuda a comparar as responsabilidades que Shuttle assume por você.
Custos e limites: o que verificar
Não escolha uma plataforma apenas pelo preço destacado na página inicial. Antes de colocar tráfego real, verifique no plano atual:
- minutos ou capacidade de build;
- CPU e memória disponíveis;
- comportamento de suspensão e cold start;
- armazenamento e conexões do PostgreSQL;
- transferência de dados;
- número de projetos e ambientes;
- retenção de logs;
- regiões disponíveis;
- domínio personalizado e TLS;
- backups e processo de restore;
- suporte e SLA, se necessários.
Faça uma estimativa baseada em requests, tempo de CPU, conexões e volume de dados. Um serviço barato que exige reescrita urgente ao atingir o primeiro cliente pode sair caro; uma plataforma mais abstrata que evita semanas de operação pode ser econômica.
Erros comuns
Copiar um exemplo incompatível com a versão atual
Sintoma: macros, atributos ou tipos não compilam. Confira versões no Cargo.lock, documentação da release instalada e exemplos oficiais correspondentes. Não misture trechos de gerações diferentes da CLI.
Rodar migration destrutiva no startup
Uma alteração que bloqueia tabela ou remove coluna pode impedir todas as instâncias de iniciar. Separe migrations pesadas e use mudanças compatíveis em etapas.
Acoplar handlers ao SecretStore
Extraia os valores no ponto de composição e construa um cliente tipado. O handler precisa de EmailClient, por exemplo, não de acesso genérico a todos os secrets do projeto.
Usar conexão de banco sem limites
Configure o pool para o tamanho da aplicação e do plano. Muitas instâncias com pools grandes podem esgotar as conexões do PostgreSQL mesmo com pouco tráfego.
Não ter endpoint de saúde
Sem uma rota simples, o diagnóstico mistura indisponibilidade do processo, falha de banco e erro de regra de negócio. Separe liveness de readiness quando a operação justificar.
Assumir que a plataforma cuida da segurança da aplicação
Infraestrutura gerenciada não implementa autorização, validação de entrada, rate limiting, política de dados ou revisão de dependências. Use cargo-audit e cargo-deny como partes de uma política maior.
Projeto de portfólio com Shuttle
Uma lista de tarefas demonstra o deploy, mas um projeto forte para entrevistas deve mostrar decisões de engenharia. Construa uma API de inventário ou chamados com:
- Axum e rotas versionadas;
- PostgreSQL com SQLx;
- migrations compatíveis;
- autenticação e autorização por função;
- validação de payload;
- paginação e filtros;
- idempotência em uma operação de escrita;
- logs estruturados;
/healthe/ready;- testes unitários e de integração;
- OpenAPI com utoipa;
- deploy no Shuttle;
- README com arquitetura, ameaças e limites;
- plano alternativo de deploy em Docker.
Em uma entrevista, não pare em “publiquei com um comando”. Explique o que a plataforma provisionou, como secrets chegam à aplicação, como migrations são aplicadas, como você detecta falhas e em qual cenário migraria para containers.
Conecte o repositório aos projetos práticos para portfólio Rust e acompanhe as vagas Rust para identificar competências pedidas em backend e plataforma.
Checklist antes de colocar a API em produção
-
cargo fmt, Clippy e testes passam; - versão de Rust e
cargo-shuttleestão controladas; - o
Routerpode ser testado fora da plataforma; - secrets reais estão fora do Git e dos logs;
-
Cargo.lockestá versionado; - migrations foram testadas com dados representativos;
- queries de lista têm paginação ou limite;
- pool PostgreSQL respeita o limite do ambiente;
- erros internos não vazam detalhes ao cliente;
- health check responde sem trabalho desnecessário;
- logs identificam operação e versão sem dados sensíveis;
- CORS usa origens explícitas;
- quotas, região, backups e restore foram conferidos;
- smoke test roda depois do deploy;
- domínio e TLS foram validados;
- existe plano de rollback ou nova publicação da versão anterior;
- a equipe sabe quando Shuttle deixa de ser o melhor encaixe.
Perguntas frequentes sobre Shuttle e Rust
O que é Shuttle no ecossistema Rust?
Shuttle é uma plataforma para construir e publicar aplicações Rust com integração à linguagem e ao Cargo. Em vez de começar pela definição de containers e servidores, o projeto declara recursos na composição do runtime e usa a CLI cargo-shuttle no desenvolvimento e no deploy.
Como publicar uma API Axum no Shuttle?
Instale e autentique a CLI, adapte o ponto de entrada para devolver ShuttleAxum, rode cargo shuttle run, teste rotas e recursos locais e publique com cargo shuttle deploy. Depois, valide a URL externa, banco, secrets, logs e comportamento de reinício.
Shuttle oferece PostgreSQL?
A plataforma possui integração para solicitar PostgreSQL e fornecer um pool à aplicação, frequentemente usado com SQLx. Confirme as features e a assinatura atuais na documentação da versão adotada, aplique migrations com cuidado e respeite limites de conexão e armazenamento.
Shuttle substitui Docker e Kubernetes?
Para muitos projetos pequenos, ele pode remover a necessidade imediata dos dois. Isso não significa que cubra qualquer topologia. Docker continua útil para portabilidade e serviços auxiliares; Kubernetes atende necessidades de orquestração e plataforma que seriam excessivas para um MVP simples.
Shuttle é bom para aprender Rust backend?
Sim, desde que a abstração de deploy não vire desculpa para ignorar HTTP, banco, concorrência, erros, testes e segurança. Use Shuttle para publicar mais cedo e dedicar tempo à qualidade da aplicação. Depois, reproduza o mesmo serviço em Docker para aprender a camada operacional que foi abstraída.
Conclusão
Shuttle oferece um caminho curto e coerente com o ecossistema Rust para publicar uma API Axum: o servidor continua sendo código Rust, PostgreSQL e secrets entram como recursos e a CLI cuida do ciclo local e do deploy. Para portfólio, MVP e serviço pequeno, essa redução de atrito pode ser mais valiosa do que controlar uma VPS desde o primeiro dia.
Use a plataforma com os mesmos padrões exigidos em qualquer produção: dependências fixadas, migrations compatíveis, secrets protegidos, pool limitado, logs estruturados, testes, health checks e validação pós-deploy. E mantenha a arquitetura portátil, concentrando a integração com Shuttle no ponto de composição.
A melhor escolha não é a infraestrutura com mais recursos, mas aquela que atende aos requisitos atuais sem esconder riscos. Se Shuttle coloca sua aplicação Rust nas mãos de usuários rapidamente e o ambiente suporta o workload, ele é um excelente ponto de partida. Quando os requisitos mudarem, uma base Axum bem separada continuará pronta para Docker, VPS ou outra plataforma.