Lembro dos meus primeiros deploys. Abrir o FileZilla, arrastar os arquivos PHP pra pasta /var/www/html, dar F5 no navegador e torcer pra não ter esquecido nada. Era tosco, mas era simples. Uma pessoa, um servidor, um arrasta-e-solta.

Interface do FileZilla — arquivos locais à esquerda, servidor remoto à direita. Arrasta e solta.

Daí a gente cresceu. E junto vieram Kubernetes, Terraform, Helm charts, service meshes, pipelines de CI/CD com 47 steps… De repente, pra colocar um CRUD no ar você precisava de um time de DevOps e uma certificação AWS.

Essa semana eu fiz o deploy de um projeto pessoal pra uma VM no Google Cloud usando o Kamal. E a sensação que eu tive foi estranhamente familiar. Era o FileZilla de novo. Só que com zero-downtime, rollback automático e sem aquele medo de “será que eu esqueci o config.php”.

O “one person framework”

O DHH vem construindo o que ele chama de one person framework há alguns anos: a ideia de que uma pessoa sozinha consegue construir, manter e colocar no ar um produto inteiro com Rails. Sem time de frontend separado. Sem time de backend separado. Sem time de infra.

O Kamal (originalmente MRSK, lançado em 2023) foi uma das primeiras peças desse quebra-cabeça — o deploy sem DevOps. Depois vieram o Solid Queue, Solid Cache e Solid Cable pra eliminar a dependência de Redis. O Rails 8 empacotou tudo junto: SQLite como banco padrão, a Solid Trifecta embutida, Propshaft e Importmaps pra matar o Webpack, e o Kamal 2 como ferramenta de deploy oficial.

Pela primeira vez, uma pessoa tem o stack completo: do rails new até a produção, sem sair do ecossistema.

SSH e Docker. Só isso.

Quando você roda bin/kamal deploy, o que acontece por baixo dos panos é quase decepcionante de tão simples:

  1. Builda a imagem Docker na sua máquina
  2. Faz push pra um container registry
  3. Conecta via SSH no servidor
  4. Roda docker pull pra baixar a imagem
  5. Roda docker run pra subir o container
  6. Troca o tráfego pro novo container

Não tem agente instalado no servidor. Não tem daemon. Não tem nada rodando em background esperando comandos. É literalmente o Kamal sentando na sua máquina, abrindo um SSH e digitando comandos Docker no servidor remoto.

É o que você faria manualmente se soubesse os comandos de cabeça. O Kamal só automatiza a sequência.

Mas o detalhe que me pegou: o Kamal não sabe que eu estou usando GCP. Ele não faz nenhuma chamada de API do Google. Não usa o gcloud CLI. Não usa Terraform. Ele só precisa de uma máquina que aceita SSH e tem Docker instalado. Pode ser GCP, AWS, DigitalOcean, Hetzner, um Raspberry Pi debaixo da sua mesa. Tanto faz.

É tipo o FileZilla — ele também não sabia se o servidor era da Locaweb ou do GoDaddy. Ele só precisava do IP, usuário e senha.

O deploy na prática

Meu projeto, o MapWise (editor de mapas com tracking em tempo real, import de CSV e chat com IA), é um Rails 8 com SQLite, Solid Queue e Action Cable. O pacote completo do one person framework. Pra colocar ele no ar numa VM e2-small do GCP (2 vCPU, 2GB RAM), eu precisei de:

Uma VM com Docker instalado

Qualquer VM Linux serve:

gcloud compute instances create mapwise \
  --machine-type=e2-small \
  --image-family=ubuntu-2404-lts-amd64 \
  --image-project=ubuntu-os-cloud

SSH na VM, instalar Docker, sair. Nunca mais você precisa entrar nela.

Um container registry

Um lugar pra guardar a imagem Docker entre a sua máquina e o servidor. Eu usei o Artifact Registry do GCP, mas Docker Hub serve igual:

gcloud artifacts repositories create mapwise \
  --repository-format=docker \
  --location=us-central1

Três arquivos de config

O config/deploy.yml — o coração:

service: mapwise
image: edy-ai-playground/mapwise/mapwise

servers:
  web:
    - 34.135.208.39
  job:
    hosts:
      - 34.135.208.39
    cmd: bin/jobs

registry:
  server: us-central1-docker.pkg.dev
  username: oauth2accesstoken
  password:
    - KAMAL_REGISTRY_PASSWORD

env:
  secret:
    - RAILS_MASTER_KEY
  clear:
    WEB_CONCURRENCY: 2

volumes:
  - "mapwise_storage:/rails/storage"

ssh:
  user: edy
  keys:
    - ~/.ssh/google_compute_engine

O .kamal/secrets:

RAILS_MASTER_KEY=$(cat config/master.key)
KAMAL_REGISTRY_PASSWORD=$(gcloud auth print-access-token)

E uma linha no config/environments/production.rb:

config.hosts.clear  # permite acesso por IP

Um comando

bin/kamal setup

Dois minutos depois, app no ar. Próximas atualizações? bin/kamal deploy. Fim.

Zero-downtime (a parte que o FileZilla não fazia)

Até aqui era nostalgia. Agora é engenharia.

Quem fez deploy com FTP conhece o terror: você sobe os arquivos um por um, o site fica quebrado durante 30 segundos enquanto metade dos arquivos é da versão nova e metade da velha. Se dava erro no meio, o servidor ficava num estado Frankenstein.

O Kamal resolve isso com um truque esperto:

  1. O novo container sobe ao lado do antigo — os dois rodam ao mesmo tempo
  2. Um proxy reverso minúsculo (kamal-proxy) espera o health check do novo container passar
  3. O tráfego é redirecionado pro novo container
  4. O container antigo drena as conexões restantes e morre

Dois containers coexistem por alguns segundos, o proxy faz o corte, e o usuário nem percebe. Se o novo container falhar no health check, o antigo continua servindo como se nada tivesse acontecido.

O que vive no servidor

Depois do deploy, isso é tudo que existe na VM:

kamal-proxy (porta 80)
├── mapwise-web-<sha>  (Puma - requests HTTP)
└── mapwise-job-<sha>  (Solid Queue - jobs em background)
    └── volume: mapwise_storage:/rails/storage

Três containers. Uma rede Docker. Um volume pro SQLite. Sem Nginx. Sem Passenger. Sem Capistrano. Sem sudo systemctl restart. O kamal-proxy cuida do roteamento e o Thruster (embutido no Rails 8) faz compressão e cache de assets.

Se você já configurou um servidor com Nginx + Passenger + Capistrano + rbenv + systemd, sabe a dor. O Kamal troca tudo isso por Docker e pronto.

Os tombos

Nenhum tutorial mostra os erros que você vai tomar. Então fica documentado:

O campo image não inclui o servidor do registry

O Kamal prefixa automaticamente. Se você colocar o caminho completo us-central1-docker.pkg.dev/projeto/repo/app, ele duplica pra us-central1-docker.pkg.dev/us-central1-docker.pkg.dev/projeto/repo/app. O erro que aparece não ajuda em nada.

Chave SSH com passphrase trava o Kamal

Ele tenta ~/.ssh/id_rsa por padrão. Se essa chave tem passphrase, morre com Errno::ENODEV. Esse erro não te diz nada — você fica achando que é problema de rede, de permissão, de firewall. Na verdade é só o Kamal tentando ler a chave, encontrando a passphrase, e não sabendo o que fazer porque não tem como abrir um prompt interativo pra você digitar.

A solução é apontar pra uma chave sem passphrase. E se você está usando GCP, você provavelmente já tem uma. Quando você roda gcloud compute ssh pela primeira vez, o gcloud gera automaticamente um par de chaves em ~/.ssh/google_compute_engine (sem passphrase) e sobe a chave pública pro metadata da VM. É por isso que gcloud compute ssh funciona sem você configurar nada — ele cuida de tudo silenciosamente.

Essa mesma chave funciona com SSH puro, e portanto com o Kamal:

ssh:
  keys:
    - ~/.ssh/google_compute_engine

Se você não está no GCP, a ideia é a mesma: gere uma chave sem passphrase (ssh-keygen -t ed25519 -N "") e aponte pra ela.

Docker Desktop precisa estar rodando

O Kamal builda a imagem localmente. Sem Docker Desktop ligado, falha silenciosamente no docker buildx build.

O build vem do último git commit, não do working directory

Alterou o deploy.yml mas não commitou? A config nova não vai pro build. Isso é intencional — o Kamal quer garantir que você está deployando código versionado. Mas te pega de surpresa na primeira vez.

A volta à simplicidade

O que me fascina no Kamal é que ele não inventa nada. SSH existe desde 1995. Docker desde 2013. Proxy reverso é conceito dos anos 90. O Kamal só juntou tudo num workflow que respeita a escala real do seu projeto.

Eu acredito que a gente complica deployment porque a indústria tem incentivo pra vender complexidade. Cada camada nova é um produto novo, uma certificação nova, um time novo. Mas a pergunta honesta é: você precisa disso?

Kubernetes é incrível — pra quem tem 50 microserviços e precisa de auto-scaling horizontal. Pra um app Rails com SQLite num servidor só? É um canhão pra matar formiga.

Pra 90% dos projetos, deploy é SSH + Docker. O Kamal só torna isso ergonômico. E a sensação de rodar bin/kamal deploy e ver seu app no ar em dois minutos me lembrou de algo que a gente perdeu pelo caminho: colocar software no ar deveria ser simples.

Como nos tempos de FTP — só que dessa vez, sem o medo.

Por hoje é só.