SQLite + Kamal: Deploy de Rails sem Drama | Bastidores do The M.Akita Chronicles
Este post vai fazer parte de uma série; acompanhe pela tag /themakitachronicles. Esta é a parte 7.
E não deixe de assinar minha nova newsletter The M.Akita Chronicles!
–
Se eu te dissesse que dá pra rodar uma aplicação Rails completa em produção — com banco de dados, fila de jobs, cache — num único VPS de $12/mês sem instalar PostgreSQL, Redis, ou qualquer outro serviço externo, você acreditaria?
Pois é. Esse é o Rails 8 com SQLite e Kamal.
A Revolução Silenciosa do SQLite no Rails
O Rails 8 trouxe o SQLite como opção real de produção, não como brinquedo de desenvolvimento. E quando digo “real”, quero dizer que o rails new gera tudo pronto: banco principal em SQLite, SolidQueue (jobs) em SQLite, SolidCache (cache) em SQLite, SolidCable (WebSocket) em SQLite.
Quatro serviços que antes precisavam de PostgreSQL + Redis + Memcached agora são arquivos .sqlite3 no disco.
O WAL mode (Write-Ahead Logging) habilita leituras concorrentes enquanto uma escrita acontece. Pra aplicações com dezenas de milhares de requisições por dia — que é a imensa maioria das aplicações Rails no mundo — SQLite é mais que suficiente. E a latência de leitura é medida em microsegundos, não milissegundos. É acesso a disco local, não round-trip de rede.
# database.yml — produção com SQLite
production:
primary:
<<: *default
database: storage/production.sqlite3
queue:
<<: *default
database: storage/production_queue.sqlite3
migrations_paths: db/queue_migrate
cache:
<<: *default
database: storage/production_cache.sqlite3
migrations_paths: db/cache_migrateTrês arquivos. Sem conexão TCP. Sem pooling. Sem “o PostgreSQL tá aceitando conexões?”. Backup é um VACUUM INTO e pronto — um comando SQL que produz uma cópia atômica. Restore? Cola o arquivo de volta. Tenta fazer isso com um dump de PostgreSQL de 2GB.
Kamal: Docker Deploy Sem Kubernetes
O Kamal é o deployer do Rails 8. Pensa nele como “SSH + Docker, mas inteligente”. Sem Kubernetes. Sem Helm charts. Sem YAML de 300 linhas descrevendo um Ingress Controller.
A configuração inteira cabe em um arquivo:
# config/deploy.yml
service: minha-app
image: meuregistry/minha-app
servers:
web:
hosts:
- 107.170.70.49
options:
network: minha-network
volumes:
- minha-app-storage:/rails/storage
- minha-app-content:/rails/content
proxy:
ssl: true
host: app.meudominio.com
builder:
arch: amd64
env:
clear:
RAILS_LOG_LEVEL: info
secret:
- RAILS_MASTER_KEY
- DATABASE_URLO que o Kamal faz quando você roda kamal deploy:
- Builda a imagem Docker localmente (ou em CI)
- Pusha pro registry
- SSH no servidor
- Puxa a nova imagem
- Roda migrations
- Reinicia o container com zero downtime via kamal-proxy
O kamal-proxy é o segredo: ele funciona como reverse proxy na frente dos seus containers. Quando o deploy acontece, ele sobe o novo container, espera ele ficar healthy, redireciona o tráfego, e depois derruba o antigo. Seus usuários não percebem nada.
Volumes Docker: O Segredo do SQLite em Produção
O ponto crítico de SQLite em Docker é: os dados não podem morrer com o container. Docker volumes resolvem isso:
volumes:
- minha-app-storage:/rails/storageO diretório storage/ contém todos os bancos SQLite. Com o volume Docker, eles persistem entre deploys. O container é efêmero; os dados são permanentes.
Mas tem um detalhe que pega muita gente: se você roda dois serviços diferentes que compartilham dados via arquivos (não banco), precisa de um volume compartilhado:
# Serviço A escreve conteúdo
volumes:
- content-compartilhado:/rails/content
# Serviço B lê conteúdo
volumes:
- content-compartilhado:/rails/contentO mesmo volume Docker montado em dois containers. Simples, funciona, e não precisa de NFS, S3, ou qualquer coisa sofisticada. É um diretório no disco do servidor.
Múltiplos Serviços no Mesmo VPS
Uma vantagem pouco discutida do Kamal: você pode rodar múltiplas aplicações no mesmo servidor. O kamal-proxy roteia por hostname:
app.meudominio.com → container-app
bot.meudominio.com → container-botCada serviço tem seu próprio deploy.yml, sua própria imagem Docker, seu próprio ciclo de deploy. Mas todos rodam no mesmo VPS, compartilhando a mesma rede Docker para comunicação interna.
O SSL é automático via Let’s Encrypt — o kamal-proxy cuida disso. Cada hostname ganha seu certificado sem você configurar nada.
Hooks: Automação no Deploy
O Kamal suporta hooks em vários pontos do ciclo de deploy. O mais útil é o pre-deploy, que roda antes do container novo substituir o antigo:
# .kamal/hooks/pre-deploy
#!/bin/sh
# Roda migrations antes do container ficar ativo
ssh root@$KAMAL_HOSTS \
"docker exec minha-app-web bin/rails db:migrate"Outros hooks úteis:
post-deploy— notificar equipe, limpar cachepre-connect— verificar saúde do servidordocker-setup— instalar dependências no host
O Dockerfile que o Rails Gera
O Rails 8 gera um Dockerfile otimizado de produção. Alguns destaques:
# Multi-stage build: build stage grande, runtime stage mínima
FROM ruby:3.3-slim AS base
# Instala apenas dependências de runtime
RUN apt-get install -y libsqlite3-0
FROM base AS build
# Aqui instala tudo pra compilar gems nativas
RUN apt-get install -y build-essential libsqlite3-dev
# Copia e instala gems
COPY Gemfile* ./
RUN bundle install --without development test
# Stage final: só o necessário pra rodar
FROM base
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY . .
RUN bundle exec bootsnap precompile --gemfile app/ lib/Multi-stage build significa que a imagem final não tem compiladores, headers de desenvolvimento, ou qualquer coisa desnecessária. A imagem de produção fica enxuta.
Secrets: Sem .env em Produção
O Kamal tem um sistema de secrets que lê de um arquivo local (.kamal/secrets) e injeta como variáveis de ambiente no container. Esse arquivo nunca vai pro git:
# .kamal/secrets (gitignored)
RAILS_MASTER_KEY=abc123...
AWS_ACCESS_KEY_ID=AKIA...No deploy.yml, você referencia:
env:
secret:
- RAILS_MASTER_KEY
- AWS_ACCESS_KEY_IDO Kamal lê do arquivo local e configura no servidor. Simples, seguro, sem HashiCorp Vault ou AWS Secrets Manager (a menos que você queira — Kamal suporta adaptadores).
Backup de SQLite: Absurdamente Simples (Se Fizer Certo)
Quer backup? Um comando SQL:
VACUUM INTO '/tmp/backup/newsletter.sqlite3';O VACUUM INTO é atômico — produz uma cópia consistente e desfragmentada do banco, mesmo com escritas acontecendo. Roda enquanto a aplicação está servindo requests normalmente. E a cópia resultante é um banco SQLite completo e funcional — abre, consulta, restaura.
Atenção: copiar o arquivo .sqlite3 diretamente (cp, tar, rsync) de um banco em uso pode corromper o backup. Se uma escrita estiver em andamento no momento da cópia, você fica com um arquivo meio-escrito. O SQLite tem WAL (Write-Ahead Log) e journaling que protegem o banco ativo — mas a cópia crua não herda essa proteção.
Na prática, automatizei isso com um job Rails que roda a cada hora:
class BackupDatabaseJob < ApplicationJob
def perform
backup_dir = File.join(Rails.configuration.content_dir, "backups")
FileUtils.mkdir_p(backup_dir)
dest = File.join(backup_dir, "newsletter.sqlite3")
ActiveRecord::Base.connection.execute("VACUUM INTO '#{dest}'")
end
endO backup vai pro diretório compartilhado de conteúdo — o mesmo que já é sincronizado com rsync pra máquina local. Sem agente de backup extra, sem serviço externo, sem snapshot de volume. Um SQL, um arquivo, um rsync.
Compara isso com pg_dump de um banco PostgreSQL de produção com dezenas de tabelas e constraints. O SQLite backup é um comando que produz um arquivo funcional. Restaurar? Copia de volta e reinicia.
Quando NÃO Usar SQLite
Vou ser honesto: SQLite não é pra tudo.
- Múltiplos servidores web escrevendo no mesmo banco? Não. SQLite é single-writer. Se você precisa de horizontal scaling com múltiplos nós, vai de PostgreSQL.
- Datasets massivos (centenas de GB)? PostgreSQL tem melhor query planning e paralelismo.
- Replicação e alta disponibilidade? PostgreSQL com streaming replication.
Mas a pergunta honesta é: quantas aplicações realmente precisam disso? A vasta maioria roda num único servidor e nunca vai precisar de mais. E pra essas, SQLite + Kamal é a combinação mais produtiva que existe hoje.
O Custo Real
Vamos fazer a conta:
| Componente | Antes | Agora |
|---|---|---|
| Servidor | VPS $24/mês | VPS $12/mês |
| Banco | RDS PostgreSQL $30/mês | SQLite (incluso) |
| Redis | ElastiCache $15/mês | SolidQueue (incluso) |
| Cache | ElastiCache $15/mês | SolidCache (incluso) |
| Deploy | CI/CD complexo | kamal deploy |
| SSL | Certbot + cron | Automático |
| Total | ~$84/mês + dor de cabeça | $12/mês |
E o custo cognitivo? Ao invés de debugar “por que o Redis perdeu meus jobs?”, “por que o PostgreSQL tá rejeitando conexões?”, “por que o cert expirou?”, você foca no que importa: o produto.
Conclusão
SQLite + Kamal não é um downgrade. É a realização de que a maioria das aplicações nunca precisou da complexidade que achávamos necessária. O Rails 8 abraçou isso e entregou uma experiência de deploy que é, sem exagero, a mais simples que já existiu no ecossistema Ruby.
Um VPS. Um comando. Zero serviços externos. E uma aplicação que roda tão rápido quanto qualquer setup enterprise com 15 serviços no docker-compose.
No próximo post, vou contar as dores de cabeça de rodar um modelo de IA em GPU na nuvem — spoiler: não é tão simples quanto os tutoriais prometem.