Ao final deste artigo você vai conseguir pegar um docker-compose.yml simples e escrever os manifestos equivalentes para Kubernetes, sabendo o que cada peça faz e onde o modelo mental muda. Quem já usa Compose tem meio caminho andado, porque os conceitos se parecem mais do que os nomes sugerem.
O ponto de partida
Imagine uma API, um Postgres e um Redis rodando no seu laptop:
# compose.yaml
services:
api:
image: myapp/api:1.2.0
ports:
- "8000:8000"
environment:
LOG_LEVEL: INFO
DATABASE_URL: postgres://user:pass@db:5432/app
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
db:
image: postgres:17
environment:
POSTGRES_DB: app
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
volumes:
- dbdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d app"]
interval: 5s
retries: 10
cache:
image: redis:7-alpine
volumes:
dbdata:
Nota: o campo version: não é mais necessário no Compose v2 e gera aviso de obsoleto. O comando é docker compose up, sem hífen.
Em Kubernetes, quase cada trecho vira um objeto separado. A tabela resume o mapa:
| Compose | Kubernetes |
|---|---|
services.api |
Deployment (os pods) + Service (o endereço estável) |
ports |
Service dentro do cluster; Ingress ou Gateway para fora |
environment |
ConfigMap (não sensível) e Secret (sensível) |
volumes |
PersistentVolumeClaim (PVC) |
healthcheck |
readinessProbe, livenessProbe, startupProbe |
depends_on |
não existe; probes e retentativa na aplicação |
deploy.resources |
resources.requests e limits |
Deployment e Service: rodar e ser encontrado
O Deployment descreve quantas cópias do container você quer e como atualizá-las. O Service dá a esse conjunto de pods um nome DNS e um IP estáveis, já que os pods nascem e morrem o tempo todo.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: myapp/api:1.2.0
ports:
- containerPort: 8000
---
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector:
app: api
ports:
- port: 80
targetPort: 8000
O Service do tipo padrão (ClusterIP) só é acessível dentro do cluster. Outros pods chamam a API por http://api, do mesmo jeito que no Compose você chamava http://api:8000 pelo nome do serviço.
Para testar sem expor nada, use kubectl port-forward svc/api 8000:80 e acesse localhost:8000.
Expor para a internet: Ingress ou Gateway API
O equivalente ao ports: "8000:8000" apontando para o mundo externo é um Ingress ou, cada vez mais, um recurso da Gateway API. Um Ingress mínimo fica assim:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 80
Atenção a um detalhe de atualidade. O controlador ingress-nginx, o mais usado em tutoriais, foi anunciado como aposentado pelo projeto Kubernetes, com manutenção apenas até março de 2026. A própria comunidade recomenda migrar para a Gateway API ou para outro controlador. Se você está começando agora, vale estudar a Gateway API em vez de decorar anotações de nginx. Em clusters gerenciados (EKS, GKE, AKS), costuma haver um controlador do próprio provedor.
ConfigMap e Secret: configuração fora da imagem
No Compose as variáveis ficam no YAML. No Kubernetes elas viram objetos próprios, e o Deployment só referencia:
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
data:
LOG_LEVEL: "INFO"
---
apiVersion: v1
kind: Secret
metadata:
name: api-secret
type: Opaque
stringData:
DATABASE_URL: "postgres://user:pass@db:5432/app"
E no container, envFrom injeta todas as chaves de uma vez:
containers:
- name: api
image: myapp/api:1.2.0
envFrom:
- configMapRef:
name: api-config
- secretRef:
name: api-secret
Um aviso importante sobre Secret. Ele guarda o valor em base64, que é só codificação, não criptografia. Quem tem permissão de leitura no namespace lê tudo. Para proteger de verdade, habilite criptografia em repouso no etcd, restrinja o RBAC e, em produção, busque os segredos de um cofre externo (por exemplo com o External Secrets Operator). E nunca versione o manifesto de Secret com valores reais.
Volumes e banco de dados
O volumes: dbdata: do Compose vira um PVC, um pedido de armazenamento que o cluster atende com um volume. Para dados que precisam de identidade estável, como um banco, o objeto adequado é o StatefulSet, que cria um PVC por réplica:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: db
replicas: 1
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: postgres
image: postgres:17
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: app
- name: POSTGRES_USER
value: user
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
Na prática, muita equipe nem roda o banco dentro do cluster. Backup, failover e atualização de versão dão trabalho, e um serviço gerenciado (RDS, Cloud SQL) ou um operador maduro como o CloudNativePG resolvem isso melhor que um StatefulSet escrito à mão. Para estudar e para ambientes de teste, o exemplo acima serve bem.
Um cuidado se você trocar para o Postgres 18: a imagem oficial mudou o diretório de dados padrão, então confira a documentação da imagem antes de reaproveitar o mountPath.
depends_on não existe, e tudo bem
No Compose, depends_on ordena a subida. No Kubernetes não há ordem garantida entre Deployments, porque o cluster trabalha de forma declarativa: ele tenta levar o estado real ao desejado, o tempo todo, e pods podem ser recriados a qualquer momento. Se o banco reiniciar às 3 da manhã, nenhum depends_on salvaria a API.
A resposta é dupla. A aplicação precisa tolerar dependência indisponível (retentativa com espera crescente na conexão). E o Kubernetes usa probes para saber o estado de cada pod:
readinessProbe:
httpGet:
path: /health/ready
port: 8000
periodSeconds: 10
livenessProbe:
httpGet:
path: /health/live
port: 8000
periodSeconds: 20
failureThreshold: 3
startupProbe:
httpGet:
path: /health/startup
port: 8000
periodSeconds: 10
failureThreshold: 30
A readinessProbe decide se o pod recebe tráfego. A livenessProbe decide se ele deve ser reiniciado. A startupProbe dá tempo para aplicações lentas subirem sem que a liveness as mate. Um erro frequente é fazer a liveness checar o banco: se o banco cair, todos os pods da API reiniciam em cascata sem resolver nada. Deixe a liveness barata e local, e coloque as checagens de dependência na readiness.
Um exemplo mínimo em Flask:
from flask import Flask
app = Flask(__name__)
@app.get("/health/live")
def live():
return {"status": "alive"}, 200
@app.get("/health/ready")
def ready():
# aqui você pode testar o banco, por exemplo
return {"status": "ready"}, 200
Pod, ReplicaSet e Deployment
O Pod é a menor unidade: um ou mais containers que compartilham rede e volumes. O ReplicaSet mantém N pods vivos. O Deployment cria e gerencia ReplicaSets para dar atualização gradual (rolling update) e rollback. Você quase sempre escreve o Deployment e deixa os outros dois por conta dele. Se apagar um pod na mão, o ReplicaSet cria outro em seguida, algo que o Compose só faz com restart: always e sem noção de réplicas.
Requests e limits
No Kubernetes, requests é o que o scheduler reserva para o pod ao escolher um nó, e limits é o teto que o container pode usar.
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
Sem requests, o scheduler enxerga o pod como "gratuito" e pode lotar um nó. Ao passar do limite de memória, o container é morto por OOM. Já o limite de CPU não mata, apenas limita (throttling), e por isso muitos times definem requests de CPU e omitem o limite. É uma escolha de projeto, não uma regra.
Armadilhas comuns
Tag latest ou imagem sem tag. O pod pode subir uma versão diferente a cada reinício. Fixe a versão, ou o digest.
Réplicas fixas em tudo. Para carga variável, use um HorizontalPodAutoscaler (autoscaling/v2) apontando para o Deployment. Ele precisa de requests de CPU definidas para calcular a utilização.
Aplicar YAML solto sem organização. Quando passar de meia dúzia de arquivos, considere Helm ou Kustomize, e use kubectl diff -f antes de kubectl apply -f.
Se quiser um atalho para ver como o Compose "viraria" Kubernetes, a ferramenta Kompose gera manifestos a partir do compose.yaml. Use como ponto de partida e revise, porque o resultado raramente está pronto para produção.
Quando não usar Kubernetes
Se tudo cabe em um ou dois containers num servidor, Compose ou um serviço gerenciado de containers (Cloud Run, ECS, App Runner) entrega mais com menos manutenção. Kubernetes compensa quando você tem muitos serviços, precisa de escala automática, rollouts sem queda e um time que aguente operar o cluster, ou quando usa a versão gerenciada para dividir esse peso.
Para praticar sem custo, instale o kind ou o minikube, rode kubectl apply -f nos arquivos acima e acompanhe com kubectl get pods -w.
Próximos passos
Para provisionar o cluster e o resto da infraestrutura como código, veja Terraform: primeiros passos. E para acompanhar a saúde dos pods em produção, siga com Monitoramento com Prometheus e Grafana.
Quer aplicar isso na sua empresa? Marque uma conversa de 45 minutos em https://iauaicloud.com.br/consultoria