Por que migramos o Viajaflux do Laravel Cloud pro AWS Fargate (e o gotcha de CSS que quase me pegou)
O Viajaflux rodava em Laravel Cloud/Vapor desde sempre. Funcionava bem pra web,
mas tinha um limite que foi ficando incômodo conforme o produto cresceu: Vapor é
Lambda por baixo, e Lambda não foi feito pra processo longo. Horizon (nossas filas)
e Reverb (WebSocket) são exatamente isso — processos que precisam ficar vivos, não
uma função que responde e morre. Migramos produção pra AWS Fargate, com mais
controle sobre containers de verdade. Registro aqui a arquitetura que ficou, as
decisões que tomamos de propósito (e as que evitamos por excesso de engenharia), e
um gotcha de build que me pegou no meio do caminho.
Por que Fargate e não só “mais Lambda”
A resposta curta já está acima: Horizon e Reverb são processos de vida longa, e forçar isso em Lambda é nadar contra a maré da própria plataforma. Fargate roda containers de verdade, então a mesma imagem Docker vira quatro processos diferentes rodando em paralelo:
| Serviço | O que roda |
|---|---|
web | Requisições HTTP normais |
horizon | Filas (Consolidadoras, Operadoras, Gateways, etc.) |
scheduler | Cron do Laravel |
reverb | WebSocket |
Cada um escala de forma independente — hoje web vai de 2 a 6 tasks por CPU,
reverb de 1 a 3. Antes, no Vapor, essa separação de processo longo vs. função
efêmera era sempre um ponto de fricção.
O que ficou de pé, peça por peça
Cliente → Cloudflare → ALB → task web (Fargate) → RDS + Redis
↑
horizon / scheduler / reverb rodam em paraleloVPC com subnets públicas (ALB, NAT) e privadas (app, banco, Redis). RDS MySQL 8.4
gerenciado, ElastiCache Redis 7 fazendo cache + filas do Horizon + sessão ao mesmo
tempo, Secrets Manager guardando APP_KEY e senha do banco fora da imagem, IAM Roles
separadas pra Fargate (baixar imagem, ler segredos) e pra app (acessar S3). Tudo em
arm64/Graviton — mais barato, e já era o padrão que usávamos no mail server.
As decisões que tomamos por exclusão, não por padrão
O que não entrou na arquitetura foi tão deliberado quanto o que entrou:
- CloudFront: não. É o CDN da própria AWS, mas a gente já usa Cloudflare (CDN + cache + WAF + TLS). Rodar os dois ao mesmo tempo é custo duplicado sem ganho — clássico caso de “porque dá pra usar” não é motivo pra usar.
- Route 53: não. Cloudflare já resolve DNS. A topologia final ficou
Cloudflare (DNS + proxy + WAF + TLS) → ALB → Fargate, com um CNAME simples apontando pro ALB. Com Cloudflare em modo proxy, nem precisei correr atrás do certificado ACM de imediato. - Multi-AZ no RDS: desligado, por ora. É economia consciente, não descuido — sei exatamente que estou trocando alta disponibilidade automática por custo menor enquanto o volume não justifica o gasto extra. Fica documentado como pendência pra revisitar, não como esquecimento.
- Read replica: não incluída ainda. O banco é leitura e escrita — o “somente leitura” que às vezes aparece nas minhas ferramentas de análise é restrição da minha conexão de consulta (MCP), não da arquitetura em si. Ligar uma réplica de leitura fica pra quando o volume de relatório pedir.
O custo total ficou entre US$ 130 e 180/mês (ALB + NAT + RDS t4g.small + Redis
t4g.micro + 4 tasks Fargate arm64) — redutível desligando NAT em alta
disponibilidade e ajustando tamanho de instância, se precisar cortar mais.
Um detalhe que não é opcional: o IP de saída
Toda integração externa do Viajaflux (Wooba, Pagar.me, Asaas, Infotravel) enxerga o tráfego saindo de um único IP fixo — o Elastic IP do NAT Gateway de produção (NAT único, single-AZ). Isso importa na prática porque é literalmente o dado que eu preciso mandar pra cada fornecedor whitelistar.
O gotcha: MaryUI perdendo estilo só no build Docker
Essa foi a parte que mais me ensinou. Depois de subir a imagem, os componentes
MaryUI apareciam sem estilo nenhum em produção — mas local, rodando npm run dev, tudo funcionava normal. Comportamento clássico de “funciona na minha
máquina”, só que dessa vez com uma causa bem específica.
O Tailwind v4 do projeto está configurado pra escanear
vendor/robsontenorio/mary/src/View/Components/**/*.php — porque o MaryUI guarda o
Blade inline dentro dos próprios arquivos PHP dos componentes, não em .blade.php
separados. O problema é que o .dockerignore exclui /vendor do contexto de build.
No stage de assets da imagem Docker, esses arquivos simplesmente não existiam —
então o Tailwind fazia o purge de qualquer classe que só aparecia ali dentro. E
php artisan view:cache não resolve, porque esse Blade inline só compila em
runtime, não em build time.
O fix foi trazer explicitamente esses arquivos pro stage de assets, antes do build do CSS rodar:
# Stage 2 — assets
COPY --from=vendor /app/vendor/robsontenorio/mary/src ./vendor/robsontenorio/mary/srcConfirmei o fix de um jeito bem direto: escolhi classes canário que só existem em
componentes MaryUI (avatar-placeholder, file-input, decoration-wavy) e chequei
o CSS final. Sem o fix, 448KB e nenhuma dessas classes. Com o fix, 470KB e as três
presentes.
Minha conclusão
O gotcha do MaryUI resume bem o motivo de eu documentar decisão de infra em vez de
só “fazer funcionar”: não foi um bug de código, foi uma interação entre duas
otimizações legítimas (.dockerignore enxuto, Tailwind fazendo purge agressivo)
que ninguém planejou colidir. O mesmo vale pra arquitetura inteira — cada “não” que
tomei (CloudFront, Route 53, Multi-AZ por ora) é uma aposta consciente, não
esquecimento, e só continua sendo uma boa aposta se eu lembrar o motivo depois. É
por isso que essa doc de infra vale tanto quanto o Terraform em si.