Kubernetes para DigitalOcean: Guia Completo de Deploy
Introdução
Kubernetes se consolidou como o padrão da indústria para orquestração de containers. Mas quem já tentou configurar um cluster do zero em bare metal sabe que a complexidade operacional é enorme: certificados TLS, etcd, networking, upgrades de control plane. É o tipo de trabalho que consome semanas e não entrega valor direto ao produto.
A DigitalOcean resolveu esse problema com o DOKS (DigitalOcean Kubernetes Service), um Kubernetes gerenciado que elimina a gestão do control plane e cobra zero pelo master node. Você paga apenas pelos worker nodes, load balancers e volumes que consumir. Para startups, equipes enxutas e projetos que precisam de infraestrutura robusta sem o custo operacional de um time de SRE dedicado, essa combinação é extremamente competitiva.
Neste guia, vamos cobrir desde a criação do cluster até o deploy de uma aplicação real com TypeScript, passando por configurações de rede, storage e boas práticas que aprendi operando clusters DOKS em produção nos últimos anos.
Se você já tem familiaridade com Docker e conceitos básicos de Kubernetes, este conteúdo vai te levar ao próximo nível. Para uma visão mais ampla sobre o ecossistema DevOps, recomendo o post DevOps para Devs: Docker, CI/CD, Kubernetes e AWS em Produção.
Por que usar Kubernetes na DigitalOcean
Custo previsível e transparente
Diferente da AWS (EKS cobra USD 0,10 por hora só pelo control plane), o DOKS oferece o control plane gratuitamente. Um cluster com três nodes de 4 GB de RAM custa aproximadamente USD 36 por mês. Você consegue rodar aplicações em produção com alta disponibilidade por um valor que cabe no orçamento de um projeto solo ou de uma startup early stage.
Integração nativa com o ecossistema DigitalOcean
O DOKS se integra nativamente com outros serviços da plataforma: DigitalOcean Container Registry (DOCR) para imagens privadas, Managed Databases para PostgreSQL e MySQL, Spaces para armazenamento de objetos, e Load Balancers que são provisionados automaticamente via Kubernetes Services do tipo LoadBalancer. Tudo dentro da mesma VPC, com latência mínima.
Casos de uso onde essa combinação brilha
Na prática, vi o DOKS entregar excelentes resultados em cenários específicos: aplicações SaaS multi-tenant que precisam escalar horizontalmente, APIs de alta disponibilidade para fintechs em estágio inicial, pipelines de processamento de dados com workers que escalam sob demanda, e ambientes de staging que replicam produção com custo reduzido.
Setup do cluster DOKS com doctl
Instalação e autenticação
Primeiro, instale a CLI da DigitalOcean e autentique:
# Instalar doctl (macOS)
brew install doctl
# Autenticar com token da API
doctl auth init
# Verificar conexão
doctl account getCriação do cluster Kubernetes
Vamos criar um cluster com pool de nodes otimizado para workloads de API:
# Listar versões disponíveis do Kubernetes
doctl kubernetes options versions
# Criar cluster com 3 nodes
doctl kubernetes cluster create vdc-production \
--region nyc1 \
--version 1.31.1-do.3 \
--node-pool "name=api-pool;size=s-4vcpu-8gb;count=3;auto-scale=true;min-nodes=2;max-nodes=6" \
--tag env:production
# Configurar kubeconfig automaticamente
doctl kubernetes cluster kubeconfig save vdc-production
# Verificar nodes
kubectl get nodes -o wideO auto-scale configurado permite que o cluster cresça de 2 até 6 nodes conforme a demanda. O DOKS cuida do provisionamento e draining automaticamente.
Configuração do Container Registry
Para armazenar suas imagens Docker de forma privada:
# Criar registry
doctl registry create vdc-registry --region nyc1
# Integrar registry com o cluster (cria o secret de pull automaticamente)
doctl registry kubernetes-manifest | kubectl apply -f -
# Login local para push de imagens
doctl registry loginExemplo prático: Deploy de API TypeScript com Express
Estrutura do projeto
Vamos criar uma API de healthcheck e métricas que serve como base para qualquer microserviço em produção. O código TypeScript a seguir implementa uma API com endpoints de health, readiness e métricas customizadas:
// src/server.ts
import express, { Request, Response, NextFunction } from "express";
interface HealthStatus {
status: "healthy" | "degraded" | "unhealthy";
uptime: number;
timestamp: string;
version: string;
checks: Record<string, boolean>;
}
const app = express();
const PORT = parseInt(process.env.PORT || "3000", 10);
const VERSION = process.env.APP_VERSION || "1.0.0";
const startTime = Date.now();
let isReady = false;
// Simula inicialização assíncrona (conexão com DB, warm-up de cache)
setTimeout(() => {
isReady = true;
console.log("Application is ready to receive traffic");
}, 5000);
// Liveness probe: o processo está rodando?
app.get("/healthz", (_req: Request, res: Response) => {
const health: HealthStatus = {
status: "healthy",
uptime: Math.floor((Date.now() - startTime) / 1000),
timestamp: new Date().toISOString(),
version: VERSION,
checks: {
server: true,
memory: process.memoryUsage().heapUsed < 500 * 1024 * 1024,
},
};
const isHealthy = Object.values(health.checks).every(Boolean);
health.status = isHealthy ? "healthy" : "degraded";
res.status(isHealthy ? 200 : 503).json(health);
});
// Readiness probe: o pod está pronto para receber tráfego?
app.get("/readyz", (_req: Request, res: Response) => {
if (isReady) {
res.status(200).json({ ready: true });
} else {
res.status(503).json({ ready: false, reason: "warming up" });
}
});
// Endpoint de métricas para monitoramento
app.get("/metrics", (_req: Request, res: Response) => {
const memUsage = process.memoryUsage();
res.json({
memory: {
heapUsedMB: Math.round(memUsage.heapUsed / 1024 / 1024),
heapTotalMB: Math.round(memUsage.heapTotal / 1024 / 1024),
rssMB: Math.round(memUsage.rss / 1024 / 1024),
},
uptime: Math.floor((Date.now() - startTime) / 1000),
pid: process.pid,
nodeVersion: process.version,
});
});
// Rota principal da API
app.get("/api/v1/status", (_req: Request, res: Response) => {
res.json({
service: "vdc-api",
version: VERSION,
environment: process.env.NODE_ENV || "development",
});
});
// Middleware de erro global
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error(`Unhandled error: ${err.message}`);
res.status(500).json({ error: "Internal Server Error" });
});
app.listen(PORT, "0.0.0.0", () => {
console.log(`Server running on port ${PORT} (version ${VERSION})`);
});Dockerfile multi-stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY src ./src
RUN npm run build
FROM node:20-alpine AS production
RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -D appuser
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]Manifesto Kubernetes completo
Crie o arquivo k8s/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: vdc-api
labels:
app: vdc-api
version: v1
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
selector:
matchLabels:
app: vdc-api
template:
metadata:
labels:
app: vdc-api
version: v1
spec:
containers:
- name: vdc-api
image: registry.digitalocean.com/vdc-registry/vdc-api:1.0.0
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: APP_VERSION
value: "1.0.0"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
livenessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 10
periodSeconds: 15
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: vdc-api-service
annotations:
service.beta.kubernetes.io/do-loadbalancer-size-slug: "lb-small"
service.beta.kubernetes.io/do-loadbalancer-protocol: "http"
service.beta.kubernetes.io/do-loadbalancer-healthcheck-path: "/healthz"
spec:
type: LoadBalancer
selector:
app: vdc-api
ports:
- protocol: TCP
port: 80
targetPort: 3000Build, push e deploy
# Build da imagem
docker build -t registry.digitalocean.com/vdc-registry/vdc-api:1.0.0 .
# Push para o DOCR
docker push registry.digitalocean.com/vdc-registry/vdc-api:1.0.0
# Deploy no cluster
kubectl apply -f k8s/deployment.yaml
# Acompanhar rollout
kubectl rollout status deployment/vdc-api
# Verificar pods e serviço
kubectl get pods -l app=vdc-api
kubectl get svc vdc-api-serviceO Load Balancer da DigitalOcean será provisionado automaticamente. Em aproximadamente dois minutos, você terá um IP externo acessível.
Exemplo prático: Script de automação para gestão do cluster
O segundo exemplo demonstra como usar a API da DigitalOcean via TypeScript para automatizar operações de infraestrutura:
// src/infra/cluster-manager.ts
import { createApiClient } from "dots-wrapper";
interface ClusterInfo {
id: string;
name: string;
region: string;
nodeCount: number;
status: string;
version: string;
}
interface ScaleResult {
success: boolean;
previousCount: number;
newCount: number;
message: string;
}
async function getClusterStatus(
client: ReturnType<typeof createApiClient>,
clusterId: string
): Promise<ClusterInfo> {
const {
data: { kubernetes_cluster: cluster },
} = await client.kubernetes.getKubernetesCluster({ kubernetes_cluster_id: clusterId });
const totalNodes = cluster.node_pools.reduce(
(sum: number, pool: { count: number }) => sum + pool.count,
0
);
return {
id: cluster.id,
name: cluster.name,
region: cluster.region,
nodeCount: totalNodes,
status: cluster.status.state,
version: cluster.version,
};
}
async function scaleNodePool(
client: ReturnType<typeof createApiClient>,
clusterId: string,
nodePoolId: string,
desiredCount: number
): Promise<ScaleResult> {
const {
data: { node_pool: currentPool },
} = await client.kubernetes.getNodePool({
kubernetes_cluster_id: clusterId,
node_pool_id: nodePoolId,
});
const previousCount = currentPool.count;
if (desiredCount === previousCount) {
return {
success: true,
previousCount,
newCount: desiredCount,
message: "Node count already matches desired state",
};
}
await client.kubernetes.updateNodePool({
kubernetes_cluster_id: clusterId,
node_pool_id: nodePoolId,
name: currentPool.name,
count: desiredCount,
tags: currentPool.tags,
});
return {
success: true,
previousCount,
newCount: desiredCount,
message: `Scaled from ${previousCount} to ${desiredCount} nodes`,
};
}
async function main(): Promise<void> {
const token = process.env.DO_API_TOKEN;
if (!token) {
throw new Error("DO_API_TOKEN environment variable is required");
}
const client = createApiClient({ token });
const clusterId = process.env.CLUSTER_ID || "";
const status = await getClusterStatus(client, clusterId);
console.log("Cluster status:", JSON.stringify(status, null, 2));
// Escalar baseado em horário (exemplo: reduz nodes fora do horário comercial)
const hour = new Date().getHours();
const isBusinessHours = hour >= 8 && hour < 20;
const targetNodes = isBusinessHours ? 4 : 2;
console.log(
`Current hour: ${hour}. Business hours: ${isBusinessHours}. Target: ${targetNodes} nodes`
);
}
main().catch(console.error);Esse tipo de automação é útil para reduzir custos em ambientes de staging ou desenvolvimento, escalando para baixo fora do horário comercial e voltando ao tamanho adequado durante o dia.
Boas práticas para DOKS em produção
Resource requests e limits são obrigatórios
Nunca faça deploy sem definir requests e limits. Sem eles, o scheduler do Kubernetes não consegue tomar decisões inteligentes de alocação, e um único pod pode consumir todos os recursos de um node, derrubando outros workloads.
Use Pod Disruption Budgets
Para garantir disponibilidade durante upgrades de nodes ou manutenções:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: vdc-api-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: vdc-apiConfigure o Cluster Autoscaler corretamente
O DOKS suporta Cluster Autoscaler por node pool. No exemplo deste guia, o api-pool já foi criado com autoscaling habilitado, mínimo de dois e máximo de seis nodes. O autoscaler adiciona capacidade quando há pods que não conseguem ser agendados e remove nodes quando consegue realocar os workloads, respeitando as restrições de agendamento e o PodDisruptionBudget. Ajuste os limites à capacidade necessária e ao orçamento, considerando também as quotas da conta.
Esse mecanismo ajusta os nodes. Para variar automaticamente as réplicas da API, configure um Horizontal Pod Autoscaler com as métricas adequadas. Mantenha os requests coerentes com a aplicação e acompanhe pods pendentes e eventos do cluster: habilitar autoscaling não garante que qualquer pod terá capacidade disponível imediatamente.
Guias Relacionados
Explore mais guias técnicos
Mais de 100 guias práticos de integração entre frameworks, ferramentas e plataformas.
Ver todos os guias