O espectro de deploy
Não existe uma única “nuvem”. Existe um espectro de quanto da máquina você controla, e cada produto se encaixa em algum ponto desse espectro.
Um virtual private server (VPS) é uma máquina que você aluga e administra. Você instala o runtime, configura um reverse proxy, gerencia certificados TLS e mantém o OS atualizado. É barato, flexível e totalmente seu para quebrar. É a escolha certa quando você precisa de um runtime específico ou quer entender cada camada.
Um platform as a service (PaaS) como Railway, Render ou Fly.io pega seu código ou imagem e o executa. Você declara uma porta e um health check; a plataforma cuida do TLS, roteamento, reinicializações e escalonamento. É por aqui que a maioria das equipes pequenas deve começar.
Containers gerenciados como AWS ECS, Google Cloud Run ou Azure Container Apps executam uma imagem de container com mais ajustes do que um PaaS: networking, IAM roles, regras de autoscaling. Eles ficam entre um PaaS e um cluster em termos de peso operacional.
Serverless functions como AWS Lambda ou Cloudflare Workers executam uma função por requisição, escalam para zero e cobram por invocação. São excelentes para tarefas esporádicas e de curta duração, mas complicadas para conexões persistentes e uso intenso de CPU.
Kubernetes é um orquestrador de propósito geral. Ele oferece o maior controle e a maior superfície de ataque/gestão. É justificado quando você opera muitos serviços com uma equipe de plataforma; é exagero para uma única API.
A maneira de escolher é começar pelo lado gerenciado e migrar para mais controle apenas quando surgir uma limitação concreta. O erro mais comum é adotar Kubernetes para um único serviço apenas porque parece a escolha “profissional”.
| Opção | Você gerencia | Ideal para | Atenção a |
|---|---|---|---|
| VPS | OS, runtime, proxy, TLS | Controle total, runtimes customizados | Patching, failover, on-call |
| PaaS | Apenas a aplicação | Equipes pequenas, iteração rápida | Menos controle, custo por unidade |
| Containers gerenciados | Imagem, IAM, regras de scaling | Serviços que precisam de ajustes finos | Mais configuração que um PaaS |
| Serverless | Uma função por vez | Tarefas esporádicas e curtas | Cold starts e limites de tempo |
| Kubernetes | Tudo acima do kernel | Muitos serviços, equipes de plataforma | Carga operacional real |
O que o termo “managed” realmente entrega
A palavra managed (gerenciado) esconde uma lista de tarefas que deixam de ser sua responsabilidade.
- Patching. O kernel, o runtime e a imagem base são atualizados sem a necessidade de você agendar uma janela de manutenção.
- TLS. Certificados são emitidos e renovados automaticamente, e o HTTP é redirecionado para HTTPS.
- Scaling. Instâncias são adicionadas quando a CPU ou a contagem de requisições ultrapassa um limite, e removidas quando esse valor cai.
- Health e restarts. Um processo que crasha ou falha no health check é substituído sem a intervenção humana.
- Logs e métricas. A saída é coletada centralmente e pode ser consultada, em vez de ficar em um arquivo em uma máquina onde você precisa entrar via SSH.
- Backups. Bancos de dados managed tiram snapshots e suportam point-in-time recovery.
O que você abre mão é do controle e de certa eficiência de custo. Você não pode tunar o kernel, instalar pacotes de sistema arbitrários ou economizar cada centavo de uma máquina. Para a maioria dos times, essa é uma troca vantajosa: a alternativa é ter um rodízio de on-call para uma infraestrutura na qual você não é especialista.
A exceção são os dados. PostgreSQL, Redis e object storage managed quase sempre valem a pena. Construir backups confiáveis, failover e replicação por conta própria é um projeto à parte, e a versão managed geralmente é mais barata do que as horas de engenharia que ela economiza.
Pensamento Twelve-factor
O “twelve-factor app” é um checklist antigo que ainda descreve com precisão a implantação cloud-native. Três de seus fatores são os mais importantes no dia a dia.
Configuração no ambiente. Tudo o que difere entre as implantações — URLs de banco de dados, chaves de API, feature flags — vem de variáveis de ambiente, não de arquivos commitados no repositório. É isso que permite que uma única imagem rode em qualquer ambiente.
DATABASE_URL=postgres://user:pass@host:5432/app
REDIS_URL=redis://host:6379
PORT=8080
Processos stateless. Uma instância não mantém estado durável. Ela pode ser iniciada, interrompida, duplicada e destruída livremente. Sessões, uploads e caches residem em serviços externos.
Logs como fluxos de eventos. A aplicação escreve linhas estruturadas no stdout e não faz mais nada com elas. A plataforma as coleta, armazena e pesquisa. Sem arquivos de log para rotacionar, sem disco para lotar.
Um quarto fator merece destaque: descartabilidade (disposability). Inicie rápido e encerre graciosamente. Um processo que leva um minuto para dar boot torna o escalonamento e os deploys lentos; um que ignora SIGTERM derruba requisições em andamento.
Criando uma imagem de container compacta
Uma boa imagem de produção é pequena, reproduzível e executa como um usuário não-root. Um build multi-stage alcança esses três objetivos.
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev
FROM node:22-bookworm-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/dist ./dist
COPY --from=build --chown=node:node /app/package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
A etapa de build instala tudo, compila e, em seguida, remove as dependências de desenvolvimento. A etapa de runtime copia apenas node_modules, dist e o manifesto. TypeScript, frameworks de teste e arquivos fonte nunca chegam à produção.
USER node remove os privilégios de root, evitando que um processo comprometido consiga escalar privilégios facilmente. NODE_ENV=production desativa comportamentos de desenvolvimento e habilita otimizações do framework. Fixe a imagem base em uma variante específica e prefira bookworm-slim ou uma imagem distroless em vez de uma instalação completa do Debian para reduzir a superfície de ataque.
Adicione um .dockerignore para que o contexto de build permaneça pequeno:
node_modules
.git
dist
.env
Uma imagem menor é baixada mais rapidamente, o que reduz diretamente o tempo de cold starts e de deploy.
Configuração de ambiente e secrets
A configuração deve ser injetada, nunca embutida. A mesma imagem deve rodar em staging e produção, alterando apenas o ambiente.
As plataformas diferem no mecanismo, mas a estrutura é a mesma. Um arquivo de configuração declara valores que não são secretos e um secret store armazena os valores sensíveis.
[env]
NODE_ENV = "production"
LOG_LEVEL = "info"
[[services]]
internal_port = 8080
Secrets são definidos separadamente e nunca commitados:
fly secrets set DATABASE_URL=postgres://...
fly secrets set STRIPE_SECRET_KEY=sk_live_...
No Kubernetes, secrets chegam como variáveis de ambiente ou arquivos montados:
envFrom:
- secretRef:
name: api-secrets
Dois hábitos mantêm isso seguro. Primeiro, valide a configuração na inicialização e falhe explicitamente se algo estiver faltando, em vez de lançar um erro na primeira requisição que precisar do valor. Segundo, prefira o acesso baseado em identidade: uma workload identity ou instance role permite que a app busque credenciais de curta duração sem a necessidade de qualquer secret estático.
import { z } from "zod";
const Env = z.object({
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url(),
PORT: z.coerce.number().default(3000),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
});
const parsed = Env.safeParse(process.env);
if (!parsed.success) {
console.error("invalid configuration", parsed.error.flatten().fieldErrors);
process.exit(1);
}
export const config = parsed.data;
O processo se recusa a iniciar com um ambiente incompleto, o que transforma uma classe de surpresas em runtime em uma falha de deploy imediata e óbvia. Nunca logue o ambiente em si: uma única linha de debug que dê dump em process.env pode vazar todos os secrets que o serviço possui.
Releases com zero-downtime e health checks
Um deploy que derruba requisições é um deploy que os usuários notam. Releases com zero-downtime dependem de duas coisas: iniciar novas instâncias antes de interromper as antigas e saber quando uma nova instância está realmente pronta.
Readiness significa que a instância pode receber tráfego. Ela já se conectou ao banco de dados, aqueceu seus caches e finalizou a inicialização. Somente então o load balancer deve rotear requisições para ela.
Liveness significa que a instância ainda está saudável. Se ela falhar repetidamente, a plataforma a encerra e a substitui.
app.get("/healthz", async (req, res) => {
try {
await db.query("select 1");
res.status(200).json({ status: "ok" });
} catch {
res.status(503).json({ status: "unhealthy" });
}
});
Mantenha o health check leve e honesto. Verificar o banco de dados é razoável; chamar cinco serviços downstream não é, pois isso transforma uma instabilidade momentânea em outro lugar em um loop de reinicialização. Se uma dependência for opcional, reporte que está saudável e degrade a funcionalidade graciosamente.
O shutdown gracioso é a outra metade do processo. Ao receber SIGTERM, pare de aceitar novas conexões, finalize as requisições em andamento, feche o pool do banco de dados e encerre a execução. Dê à plataforma um período de carência ligeiramente superior à requisição mais lenta.
process.on("SIGTERM", () => {
server.close(async () => {
await db.end();
await redis.quit();
process.exit(0);
});
setTimeout(() => process.exit(1), 15_000).unref();
});
O timeout serve como uma rede de segurança: se algo travar, o processo ainda assim será encerrado antes que o período de carência da plataforma expire e a plataforma recorra ao SIGKILL.
Escalonamento horizontal e statelessness
Escalonar horizontalmente significa executar mais cópias da mesma instância. Isso só funciona se as instâncias forem intercambiáveis, o que exige que nenhuma delas detenha um estado único.
O estado deve pertencer a serviços projetados para isso:
- Sessões no Redis, não na memória ou apenas em um cookie assinado.
- Uploads em armazenamento de objetos como S3 ou R2, não no disco local.
- Caches no Redis ou em uma CDN, não em um map local do processo.
- Background jobs em uma fila com workers dedicados, não em um timer dentro do processo web.
Uma vez que o estado é externo, o escalonamento torna-se apenas uma questão de números. A plataforma adiciona instâncias sob carga e as remove quando ela diminui, e qualquer instância pode atender a qualquer requisição.
O autoscaling precisa de um sinal. CPU é o padrão comum, mas para serviços Node.js com gargalo de I/O, a concorrência de requisições ou a profundidade da fila geralmente rastreiam melhor a carga. Escalone com base na métrica que realmente prevê a saturação e sempre defina uma contagem mínima de instâncias para que o serviço sobreviva a um pico de tráfego enquanto novas instâncias iniciam.
Lembre-se do problema de conexão: cada instância abre seu próprio pool de banco de dados. Vinte instâncias com um pool de vinte precisam de quatrocentas conexões, o que o PostgreSQL não concederá. Limite o pool por instância e coloque um pooler na frente do banco de dados.
Postgres e Redis Gerenciados
O banco de dados é a parte da stack menos adequada para ser gerenciada por conta própria, mas, paradoxalmente, a que mais nos tenta a fazer isso. Um Postgres gerenciado oferece backups automatizados, recuperação de ponto no tempo (point-in-time recovery), standby de failover e, frequentemente, réplicas de leitura, sem exigir nenhum trabalho operacional.
Duas regras mantêm a saúde do sistema. Primeiro, dimensione as conexões deliberadamente. Configure max no pool para um número pequeno por instância e utilize um pooler como o PgBouncer para deploys com múltiplas instâncias. Segundo, execute as migrations como uma etapa de release, e não no boot da aplicação. Se cada instância executar a migration na inicialização, um rolling deploy executará a mesma migration cinco vezes simultaneamente.
# run once, before the new version starts
npm run db:migrate
O Redis gerenciado é o lugar natural para sessões, contadores de rate-limit e filas de jobs. Ative a persistência se os dados forem importantes e trate a instância como uma dependência compartilhada cuja latência afeta cada requisição. Mantenha-o na mesma região da aplicação; um salto entre regiões em cada leitura de cache é um custo que você sentirá na performance.
DNS, TLS e domínios personalizados
O DNS mapeia um nome para a plataforma, e o TLS torna a conexão confiável. Ambos são amplamente automatizados hoje em dia, mas os detalhes ainda são importantes.
Aponte um registro A ou ALIAS para o endereço da plataforma, ou um CNAME para o seu hostname. Use um TTL curto durante a migração para que qualquer erro seja corrigido rapidamente e, depois, aumente-o. Mantenha o domínio apex e o host www consistentes, redirecionando um para o outro para que links e cookies não fiquem fragmentados entre duas origens.
O TLS é emitido e renovado automaticamente pela maioria das plataformas. Force o HTTPS, habilite o HSTS quando estiver confiante e certifique-se de que o app confie nos headers de encaminhamento do proxy para que ele gere URLs https e identifique o IP real do cliente.
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
Quando você gerencia seu próprio reverse proxy, o guia de Nginx aborda esses headers, a terminação TLS e a configuração de upstream em detalhes. Configurações incorretas podem causar loops de redirecionamento e fazer com que os rate limiters identifiquem todas as requisições como vindas do IP do proxy.
Observabilidade e alertas
Você não consegue operar aquilo que não consegue enxergar. Três tipos de sinais cobrem a maioria das necessidades.
Logs são eventos estruturados. Emita JSON com um nível, uma mensagem e um request id para que possam ser filtrados e correlacionados. Escreva no stdout e deixe que a plataforma os colete.
console.log(JSON.stringify({ level: "info", msg: "request", id, path, ms }));
Metrics são números ao longo do tempo: taxa de requisições, taxa de erro, percentis de latência, CPU, memória e profundidade de fila. Monitore os quatro sinais dourados — latência, tráfego, erros e saturação — para cada serviço.
Traces acompanham uma requisição através de diversos serviços e tornam visível um fan-out lento. Eles tornam-se mais importantes conforme o número de serviços cresce; para uma única API, bons logs e metrics geralmente são suficientes.
Configure alertas baseados em sintomas que os usuários sentem, não em causas. Uma taxa de erro crescente ou a latência p95 acima de um limite justifica acordar alguém. CPU alta que não afetou as requisições é uma anotação de dashboard, não um chamado urgente. Todo alerta deve ser acionável, caso contrário, as pessoas serão treinadas a ignorá-los.
Consciência de custos
As faturas de nuvem crescem nas lacunas entre as decisões. Os culpados habituais são previsíveis.
O Egress costuma ser a maior surpresa. Dados que saem do provedor são cobrados, às vezes pesadamente, enquanto a entrada de dados geralmente é gratuita. Sirva assets grandes através de um CDN, comprima as respostas e mantenha serviços que se comunicam frequentemente na mesma região.
Recursos ociosos são fáceis de esquecer: um banco de dados de staging deixado ligado, volumes não anexados, snapshots antigos, instâncias superdimensionadas mantidas “por precaução”. Reduza a escala de ambientes de não-produção fora do horário comercial.
O Over-provisioning desperdiça dinheiro na direção oposta. Ajuste o tamanho das instâncias medindo o uso real e deixe o autoscaling lidar com os picos, em vez de pagar pelo pior cenário 24 horas por dia.
Trade-offs de Serverless. Funções que escalam para zero são baratas quando ociosas, mas podem ser caras sob carga constante em comparação com um pequeno container sempre ativo. Modele ambas as opções se o tráfego for previsível.
Configure um alerta de orçamento na conta. Uma fatura que dobra silenciosamente é muito pior do que uma notificação de que um limite foi ultrapassado.
Infraestrutura como Código
Clicar em um console funciona bem para o primeiro deploy, mas torna-se um risco depois disso. A infraestrutura como código registra o estado desejado em arquivos, tornando os ambientes reproduzíveis, revisáveis e recuperáveis.
Terraform e OpenTofu descrevem recursos de forma declarativa. Pulumi utiliza linguagens de programação reais. Manifestos de Kubernetes e arquivos de configuração de PaaS são formas mais simples da mesma ideia. A ferramenta importa menos do que a prática: a definição reside no controle de versão e é aplicada por uma pipeline.
resource "aws_db_instance" "main" {
engine = "postgres"
instance_class = "db.t4g.small"
allocated_storage = 50
backup_retention_period = 7
}
Duas regras tornam o IaC seguro. Mantenha o estado remoto e travado (locked) para que duas pessoas não apliquem alterações conflitantes. Revise os planos antes de aplicá-los, pois um plano que pretenda destruir um banco de dados deve ser interrompido por um humano, e não prosseguir silenciosamente.
Para um único serviço de PaaS, um fly.toml ou render.yaml commitado no repositório já é infraestrutura como código. Comece por aí e adote uma ferramenta completa quando a complexidade aumentar.
Rollbacks e forward fixes
Todo deploy precisa de um caminho de volta. Como o artefato é imutável e tagueado por commit, fazer um rollback é redeployar o digest anterior.
fly releases
fly deploy --image ghcr.io/me/app@sha256:previous
No Kubernetes, um rollout undo retorna para a revisão anterior. Em um PaaS, a maioria das plataformas mantém uma lista de releases e permite que você redeploye uma delas com um clique.
Rollbacks são rápidos, mas nem sempre suficientes. Se uma migração de banco de dados já foi executada e alterou o schema, o código antigo ainda deve ser capaz de lê-lo. É por isso que as migrações devem ser retrocompatíveis por pelo menos uma release: adicione colunas antes de usá-las, pare de usar colunas antes de removê-las e nunca combine uma alteração destrutiva com o código que depende dela no mesmo deploy.
Quando um rollback é impossível — como em uma migração de dados que não pode ser revertida — a solução é um forward fix: envie a correção rapidamente, seguindo a mesma disciplina de pipeline, em vez de editar manualmente o ambiente de produção.
Networking, regiões e latência
Onde suas instâncias e dados residem determina a percepção de velocidade da aplicação. Uma requisição que atravessa um oceano para alcançar o banco de dados carrega, no mínimo, cem milissegundos de latência antes mesmo de qualquer processamento começar, e nenhuma otimização de aplicação consegue remover isso.
Mantenha a aplicação, o banco de dados e o cache na mesma região. Esta é a decisão de latência com maior impacto e ela é gratuita. Coloque assets estáticos atrás de um CDN para que sejam servidos de um edge próximo ao usuário, e permita que apenas requisições dinâmicas viajem até a origin.
Não exponha o banco de dados à internet pública. Use a rede privada da plataforma para que apenas serviços na mesma rede possam alcançá-lo, e permita o acesso através de security groups ou regras de firewall em vez de uma porta aberta. Isso elimina toda uma classe de ataques e, geralmente, melhora a latência simultaneamente.
Multi-region é um passo que a maioria dos produtos não precisa. Isso multiplica os custos, complica a gestão do banco de dados e introduz o replication lag, onde um usuário que escreve em uma região e lê em outra visualiza dados obsoletos. Comece com uma única região e um CDN, meça a latência que os usuários reais experimentam e expanda apenas quando um público específico exigir.
Testando um deployment antes que os usuários o vejam
A produção não deve ser o primeiro lugar onde uma alteração é executada. Algumas camadas de teste capturam falhas que os testes unitários não conseguem.
Preview environments sobem um deployment completo por pull request, permitindo que o revisor navegue pela alteração real. Plataformas que suportam esse recurso transformam a revisão de “ler um diff” em “usar a funcionalidade”.
Staging espelha a configuração de produção — mesmo engine de banco de dados, mesmo formato de variáveis de ambiente, mesmo proxy — mas sem os dados de produção. Sua função é capturar a classe de bugs que só aparece quando os componentes reais estão conectados.
Smoke tests são executados imediatamente após cada deploy e exercitam o caminho crítico: o endpoint de health, um login, uma leitura e uma escrita. Eles não são uma suíte de testes; são uma confirmação rápida de que o release está vivo.
#!/usr/bin/env bash
set -euo pipefail
BASE="${1:-https://app.example.com}"
curl -fsS "$BASE/healthz" >/dev/null
curl -fsS "$BASE/api/version" | grep -q '"sha"'
echo "smoke test passed"
Feature flags desacoplam o deployment do release. O código é enviado “no escuro”, então uma flag o ativa para um usuário, depois para uma porcentagem e, finalmente, para todos. Se algo estiver errado, a flag é desligada em segundos, sem a necessidade de um novo deploy.
Load testing antes de um lançamento revela onde está o primeiro gargalo: conexões, CPU ou uma query lenta. Teste com concorrência realista e monitore o banco de dados, não apenas a API.
Melhores práticas
- Comece com serviços gerenciados e migre para o controle total apenas quando surgir uma limitação.
- Construa uma imagem multi-stage pequena e a execute como um usuário não-root.
- Injete toda a configuração via variáveis de ambiente; nunca a incorpore na imagem.
- Mantenha cada instância stateless; coloque sessões, uploads e caches em stores gerenciados.
- Exponha endpoints simples de readiness e liveness.
- Trate
SIGTERMe feche as conexões antes de encerrar a execução. - Execute migrations como uma etapa de release e mantenha-as retrocompatíveis.
- Limite as conexões de banco de dados por instância e utilize um pool na frente do PostgreSQL.
- Identifique as releases por digest e mantenha a versão anterior pronta para rollback.
- Escreva logs estruturados no stdout e configure alertas para sintomas visíveis ao usuário.
- Defina a infraestrutura em controle de versão e revise os planos antes de aplicá-los.
- Configure um alerta de orçamento e monitore o tráfego de egress.
Erros comuns
- Adotar Kubernetes para um único serviço e gastar todo o roadmap focando no cluster.
- Armazenar sessões em memória e depois se perguntar por que os usuários são deslogados a cada deploy.
- Gravar uploads no disco local e perdê-los quando a instância é substituída.
- Embutir a configuração de ambiente na imagem e fazer o rebuild para cada ambiente.
- Executar migrations na inicialização da aplicação, fazendo com que um rolling deploy as execute várias vezes.
- Escalar instâncias sem ajustar o limite de conexões do banco de dados.
- Confiar cegamente em proxy headers ou não encaminhá-los.
- Manter o tamanho padrão do connection pool do banco de dados em uma frota de instâncias.
- Configurar alertas de CPU em vez de monitorar erros e a latência que os usuários sentem.
- Esquecer do egress e de recursos ociosos até a chegada da primeira fatura surpreendente.
- Não ter um caminho de rollback, ou ter um que falha porque uma migration não era compatível.
- Gerenciar a produção manualmente via console, sem nenhum registro do que foi alterado.
Próximos passos
Este guia é o destino do artefato que o CI/CD constrói e promove. A imagem em si vem do guia de Docker, que aborda builds multi-stage e cache de camadas em profundidade. Quando você faz a terminação de TLS ou roteia o tráfego por conta própria, o guia de Nginx mostra a configuração do proxy, e o guia de Linux explica o host e o shell que você está automatizando. Juntos, eles cobrem todo o caminho desde um commit de alteração até um release em execução e observável.