Deploy Rust em VPS: Docker, systemd e Nginx em 2026

Guia prático para publicar uma API Rust em uma VPS usando Docker multi-stage, systemd, Nginx, health checks, logs e rollback seguro.

Introdução

Deploy de Rust em produção costuma cair em dois extremos: ou um Kubernetes completo para uma aplicação pequena, ou um binário copiado por SSH sem logs, health check nem rollback. Para a maioria dos produtos pequenos e médios, uma VPS com Docker, systemd e Nginx é o ponto de equilíbrio: simples de operar, barato, reproduzível e seguro o bastante para tráfego real.

Este guia mostra um caminho pragmático para publicar uma API Rust, especialmente uma aplicação Axum ou Actix Web, em uma VPS Linux. A ideia é sair de cargo run local para um serviço com:

  • imagem Docker pequena;
  • variáveis de ambiente separadas do código;
  • Nginx com TLS na frente;
  • health check para monitoramento;
  • logs via journalctl e Docker;
  • rollback rápido quando algo der errado.

Se você ainda não otimizou a imagem, leia também o guia de Rust e Docker para builds de produção, o artigo de CI/CD para Rust e a página de Axum no ecossistema Rust quando a aplicação for uma API HTTP. Aqui vamos focar no servidor.

Arquitetura Recomendada

A topologia é simples:

Internet
   |
   v
Nginx :443  --->  container rust-api :3000
   |
   +-- /healthz para monitoramento externo

Nginx termina TLS, aplica limites básicos e encaminha as requisições para a aplicação Rust rodando em 127.0.0.1:3000 ou em uma rede Docker local. A aplicação não precisa expor porta pública diretamente.

Preparando a API Rust

Tenha pelo menos um endpoint de saúde. Em Axum:

use axum::{routing::get, Router};
use std::net::SocketAddr;

async fn healthz() -> &'static str {
    "ok"
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt()
        .json()
        .with_env_filter(std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()))
        .init();

    let app = Router::new()
        .route("/healthz", get(healthz));

    let addr: SocketAddr = "0.0.0.0:3000".parse().unwrap();
    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
    tracing::info!(%addr, "servidor iniciado");
    axum::serve(listener, app).await.unwrap();
}

O endpoint deve ser barato, sem depender de APIs externas. Se a aplicação usa Postgres ou Redis, crie um segundo endpoint mais profundo, como /readyz, para checar dependências internas quando fizer sentido.

Dockerfile Multi-Stage

Um Dockerfile mínimo e eficiente:

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 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     && 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"]

Para projetos maiores, substitua a etapa de build por cargo-chef, como no guia de builds Docker otimizados. O importante é não rodar a aplicação final dentro da imagem rust:*.

docker-compose.yml de Produção

Mesmo com um único serviço, docker compose ajuda a padronizar restart, env file e rede:

services:
  rust-api:
    image: registry.example.com/minha-api:2026-05-19
    container_name: rust-api
    restart: unless-stopped
    env_file:
      - /etc/minha-api/minha-api.env
    ports:
      - "127.0.0.1:3000:3000"
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/healthz"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 10s

O arquivo /etc/minha-api/minha-api.env deve ficar fora do repositório:

DATABASE_URL=postgres://app:[email protected]:5432/minha_api
RUST_LOG=info,tower_http=warn
APP_ENV=production

Nunca coloque secrets no Dockerfile, no docker-compose.yml versionado ou no histórico do shell.

systemd para Subir no Boot

Crie /etc/systemd/system/minha-api.service:

[Unit]
Description=Minha API Rust
After=docker.service network-online.target
Requires=docker.service
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/minha-api
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0

[Install]
WantedBy=multi-user.target

Depois:

sudo systemctl daemon-reload
sudo systemctl enable --now minha-api
sudo systemctl status minha-api

systemd garante que a stack volte após reboot. O restart: unless-stopped no Compose cuida de crashes do container.

Nginx como Reverse Proxy

Um bloco básico com proxy headers e timeouts:

server {
    listen 80;
    server_name api.exemplo.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;
        proxy_read_timeout 30s;
        proxy_connect_timeout 5s;
    }

    location /healthz {
        proxy_pass http://127.0.0.1:3000/healthz;
        access_log off;
    }
}

Depois use Certbot ou seu gerenciador preferido para TLS:

sudo certbot --nginx -d api.exemplo.com

Para APIs públicas, adicione rate limit no Nginx e limite tamanho de body:

client_max_body_size 2m;
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

location / {
    limit_req zone=api_limit burst=20 nodelay;
    proxy_pass http://127.0.0.1:3000;
}

Deploy e Rollback

Um deploy manual seguro pode ser apenas:

cd /opt/minha-api
docker compose pull
docker compose up -d
curl -fsS https://api.exemplo.com/healthz

Para rollback, mantenha a tag anterior no docker-compose.yml ou em um arquivo .env:

APP_IMAGE=registry.example.com/minha-api:2026-05-18
docker compose up -d
curl -fsS https://api.exemplo.com/healthz

Evite latest em produção. Tags imutáveis por commit SHA ou data facilitam auditoria e rollback.

Logs e Observabilidade

Com logs JSON via tracing, você consegue inspecionar rapidamente:

docker logs --tail=100 -f rust-api
journalctl -u minha-api -n 100 --no-pager

Para serviços em crescimento, envie logs para Loki, Datadog ou outro agregador. Métricas básicas podem começar com contador de requisições, latência p95 e erros 5xx. O artigo de logging e observabilidade em Rust mostra a base com tracing, OpenTelemetry e Prometheus.

Checklist de Produção

Antes de apontar tráfego real, confirme:

  • container não roda como root;
  • secrets ficam em /etc/minha-api/*.env, cofre ou secret manager;
  • /healthz responde 200;
  • Nginx usa HTTPS e encaminha X-Forwarded-*;
  • RUST_LOG está em nível adequado;
  • rollback foi testado pelo menos uma vez;
  • backups e migrações de banco foram definidos;
  • monitoramento externo alerta se /healthz falhar;
  • portas internas não estão expostas publicamente.

Erros Comuns

Rodar cargo run --release no servidor

Funciona para teste, mas mistura toolchain, código-fonte e runtime em produção. Gere imagem ou binário versionado.

Expor a porta 3000 para a internet

Prefira 127.0.0.1:3000:3000 no Compose e deixe Nginx ser a única entrada pública.

Usar latest

latest torna deploys não reprodutíveis. Use minha-api:<sha> ou minha-api:2026-05-19.

Health check que depende de terceiros

Se /healthz chama gateway de pagamento, API externa ou LLM, você transforma instabilidade externa em restart interno. Separe health básico de readiness profundo.

Conclusão

Rust já entrega binários rápidos e confiáveis; o deploy precisa preservar essas qualidades. Uma VPS com Docker, systemd e Nginx é uma base simples para produção: barata, debuggável e com rollback claro. Quando o tráfego ou a equipe crescerem, a mesma disciplina — imagem pequena, health check, logs estruturados e tags imutáveis — migra bem para Kubernetes ou plataformas gerenciadas.

Esse tipo de experiência também ajuda em vagas Rust de backend, plataforma e infraestrutura, principalmente quando você consegue explicar trade-offs de operação e não apenas sintaxe da linguagem. Se a empresa mantém serviços em várias linguagens, vale comparar o mesmo desenho com o ecossistema do Go Brasil para Docker e deploy, porque Go segue muito forte em control planes, CLIs e serviços cloud native enquanto Rust ganha espaço em componentes que pedem binário enxuto, segurança de memória e controle fino de recursos.

O próximo passo é automatizar esse fluxo com CI/CD, gerar tags por commit e publicar a imagem apenas depois de cargo test, cargo clippy e build de release passarem. Para transformar isso em portfólio, publique um repositório pequeno com README de arquitetura, Dockerfile, Compose, unidade systemd e um postmortem simulado de rollback. Depois, conecte esse projeto à sua página de empresas que usam Rust e mostre que você entende deploy, observabilidade e manutenção, não só código local.