NickaelDeveloper & Product Builder

Case study · Plataforma e hub

nickael.com

O que é

Um hub pessoal em nickael.com, e a plataforma self-hosted por baixo dele onde cada produto que eu construo ganha o próprio subdomínio.

Por que construí

Eu queria um endereço único para tudo que faço — e uma fundação onde adicionar o próximo produto seja um deploy, não mais um servidor para comprar e cuidar.

Status
No ar
Ano
2026
Papel
Tudo — arquitetura, infraestrutura, implementação, operação
Stack
Astro · TypeScript · Appwrite · Docker · Traefik

01

Contexto

Eu já operava uma VPS com uma instalação Appwrite nela, servindo um projeto existente. O que eu não tinha era onde colocar a próxima coisa.

Todo projeto paralelo que eu começava acabava em outro lugar — outro host, outra conta, outra forma de deployar, cada um com seu pequeno imposto de memorização. Nada se conectava a nada. Não existia um endereço único que eu pudesse dar a alguém e que significasse “é isto que eu construo”.

Então o objetivo nunca foi de fato “fazer um site de portfólio”. Era construir o lugar sobre o qual o portfólio ficaria.

02

O problema

O movimento óbvio era colocar o trabalho novo dentro do Appwrite que eu já tinha, como mais um projeto. Eu não queria isso. Um erro num produto não pode alcançar outro — nem o banco, nem o storage, nem os runtimes. Tenancy compartilhada torna essa garantia impossível de afirmar com honestidade.

Então o requisito ficou específico: uma segunda plataforma Appwrite completa e independente — banco, Redis, workers, executor, storage, Sites e Functions próprios — sem nada compartilhado além da própria máquina.

03

Restrições

  • Uma VPS, um IP público, um daemon Docker — tudo já em uso pela instalação existente.
  • Um Traefik já rodava como reverse proxy da máquina, ocupando as portas 80 e 443.
  • O Appwrite vem com o próprio Traefik, que quer exatamente essas portas.
  • A instalação existente servia tráfego real e não podia quebrar.
  • Operação solo. O que quer que eu construísse, eu seria quem depuraria à meia-noite.

Essa última restrição trabalhou mais que as outras. Ela descartou qualquer coisa engenhosa o bastante para eu não lembrar como funcionava seis meses depois.

04

Meu papel

Tudo: decidir que aquilo deveria existir, escolher a arquitetura, executar a instalação, diagnosticar o que quebrou e operar agora que está de pé. Não havia a quem passar uma camada.

O trabalho foi assistido por IA do começo ao fim, e vale ser preciso sobre onde fica a linha. Agentes foram bons em gerar configuração, lembrar flags e produzir explicações candidatas rapidamente. Não foram eles que decidiram manter o proxy interno do Appwrite, perceberam que um container respondendo pelo hostname errado explicava um sintoma quatro camadas adiante, ou julgaram qual de três explicações plausíveis valia testar primeiro.

05

Decisões

Manter o Traefik interno do Appwrite

Dois reverse proxies querendo as mesmas portas é um conflito que você resolve de propósito ou sofre por acidente. Eu podia remover o Traefik interno do Appwrite e religar a stack ao externo, ou manter o Appwrite perto do formato oficial e colocar o proxy externo na frente.

Eu mantive. Esse proxy interno não é decoração — ele roteia console, API, Sites, Functions, previews e custom domains. Removê-lo significa assumir cada uma dessas regras para sempre, re-derivando a cada upgrade. Em vez disso o proxy externo virou uma borda pura: entrada pública, TLS e a qual stack um hostname pertence. Tudo abaixo do hostname continuou sendo problema do Appwrite.

Uma segunda plataforma, não um segundo projeto

O Appwrite tem uma noção perfeitamente boa de projetos dentro de uma instalação, e usá-la teria levado uma tarde. Rejeitei porque ela dá isolamento de dados, mas não de falha: uma migração ruim, um worker descontrolado, um upgrade malsucedido, e os dois produtos caem juntos. Duas stacks custam mais setup uma vez; tenancy compartilhada custa risco continuamente.

TLS wildcard na borda, não ACME por site

O Appwrite pode pedir certificados próprios por domínio via HTTP-01. Desliguei isso e emiti wildcards no proxy externo via DNS-01. Um certificado cobre um namespace inteiro, subdomínios novos não precisam de etapa de emissão nenhuma, e o desafio HTTP-01 — que depende exatamente do roteamento que você ainda está configurando — sai do caminho crítico.

06

Como funciona

Como uma requisição chega a um produto InternetTraefik (externo):80 / :443 · TLS · wildcardsAppwrite ATraefik (interno) · só loopbackAPI · Sites · Functionsoutro projetoAppwrite BTraefik (interno) · só loopbackAPI · Sites · Functionsnickael.com · *.nickael.comCompartilhado: a máquina, o daemon Docker, um IP públicoComo uma requisição chega a um produto InternetTraefik (externo):80 / :443 · TLS · wildcardsAppwrite ATraefik (interno) · só loopbackAPI · Sites · Functionsoutro projetoAppwrite BTraefik (interno) · só loopbackAPI · Sites · Functionsnickael.com · *.nickael.comCompartilhado:a máquina, o daemon Docker, um IP público
Como uma requisição chega a um produto

Uma requisição chega ao proxy externo, que termina o TLS e decide — só pelo hostname — a qual instalação ela pertence. Encaminha ao Traefik interno daquela stack no loopback, e dali o Appwrite faz o próprio roteamento para a API, um Site ou uma Function.

Detalhe técnicoA única configuração que faz ou quebra tudo

O router interno escolhe a regra a partir do hostname original. Se o proxy externo encaminha sem preservar o header Host, o Traefik interno enxerga o endereço de loopback em vez do nome real — e Sites, Functions e custom domains silenciosamente deixam de resolver.

traefik · dynamic
loadBalancer:
  passHostHeader: true      # sem isso: Host: 127.0.0.1
  servers:
    - url: "https://127.0.0.1:<PORT>"

Os proxies internos escutam apenas no loopback, de modo que nada além do externo os alcança. A superfície pública da máquina continua sendo um único processo.

07

Construindo

A segunda stack começou como uma cópia da primeira, que é precisamente o motivo de ser perigosa. Duas stacks idênticas num mesmo daemon Docker dividem um namespace para coisas em que você para de pensar: nomes de container, hostnames, nomes de rede, nomes de volume, segredos internos, portas locais.

Cada um precisa ser tornado único de propósito. Esqueça um só e você não recebe um erro — recebe duas stacks dividindo silenciosamente algo que não deviam, e um sintoma que aparece em outro lugar sem relação aparente.

Detalhe técnicoO que precisa ser tornado único
  • Nome do projeto Compose — senão as duas stacks disputam os mesmos prefixos de recurso.
  • container_name E hostname — são identificadores diferentes, e mudar um não muda o outro.
  • Redes Docker, tanto de aplicação quanto de runtimes, cada uma criada antes do primeiro boot.
  • Volumes, para storage e certificados nunca caírem no mesmo lugar.
  • Segredos internos, gerados do zero em vez de copiados junto com o arquivo.
  • Bindings de porta no loopback, para os dois proxies internos não colidirem.

08

O que quebrou

Um Site de teste foi deployado, buildou, iniciou — e então não devolveu absolutamente nada. DNS resolvia, TLS completava, a requisição saía, e zero bytes voltavam até o timeout. Nenhum log dizia por quê.

Um timeout é a falha menos informativa que existe, então o primeiro movimento foi transformá-lo em algo mais específico. Requisitei um hostname que não existia:

random.sites.example-b.com   →  rule_not_found   (rápido)
hello.sites.example-b.com    →  timeout          (trava)

Essa única comparação limpou quatro camadas de uma vez. Um erro de aplicação provou que a requisição chegava ao Appwrite — logo DNS, TLS, router externo e regra wildcard já estavam certos. A falha só aparecia quando um Site real tentava alcançar um runtime.

Dali desci camada a camada. O container de runtime era criado sob demanda; os logs mostravam o servidor iniciando. Alcançá-lo direto, sem credencial, devolvia erro de autorização — prova de que estava vivo e escutando. Com o secret certo, devolvia a página inteira. Tudo funcionava. Então parei de testar a partir do host e testei de dentro do executor, que é o componente que de fato precisa fazer essa chamada em produção.

host     → runtime    ✅ funciona
executor → runtime    ❌ timeout

Esse foi o ponto de virada. Não era a aplicação, nem o Appwrite, nem o framework, nem o Traefik, nem o TLS. Era networking do Docker.

A causa raiz

Eu tinha dado à segunda stack um nome de container único e presumi que isso cobria. Não cobria. O container manteve o hostname padrão — e o executor se conecta à própria rede de runtimes procurando a si mesmo por esse hostname. Dois containers respondiam pelo mesmo nome no mesmo daemon. A busca encontrou o errado, e o executor da primeira instalação obedientemente entrou numa rede que era da segunda.

Executor A                    Executor B
├─ appwrite-primary           └─ appwrite-secondary
├─ runtimes-primary              (faltava runtimes-secondary)
└─ runtimes-secondary  ← errado

Corrigir o hostname reparou executor→runtime e imediatamente quebrou Appwrite→executor, que ainda estava configurado para alcançar o nome antigo. O sintoma mudou de timeout travado para um 500 rápido — e isso era progresso digno de nota, não uma regressão.

E então o domínio

Apontar nickael.com para a plataforma teve sua própria sequência. O apex não pode carregar CNAME — isso é regra de DNS, não do Appwrite — então levou um A record, que o Appwrite aceitou. A emissão do certificado falhou mesmo assim, e a resposta honesta é que persegui o suspeito errado por um tempo: propagação de DNS parecia culpada, e não era. A causa real era uma entrada faltando na configuração dinâmica do proxy externo. A requisição nunca chegava a nada que pudesse responder ao desafio.

09

Onde chegou

Duas plataformas Appwrite completas agora rodam numa máquina — dois bancos, dois Redis, dois executores, duas redes de runtime, dois conjuntos de Sites e Functions — atrás de um único proxy público, sem Kubernetes e sem bifurcar a stack oficial.

Este site é a primeira coisa a morar na nova. É um build estático em Astro, deployado no Appwrite Sites, servido por essa plataforma no domínio apex. A parte que importa não é o site — é que o próximo produto não precisa de nada deste trabalho de novo. Ele ganha um subdomínio e um deploy.

O site foi a parte fácil. O problema interessante era construir um lugar onde todo produto futuro pudesse morar.

10

O que eu mudaria

  • Tornar todo identificador único antes do primeiro boot, não depois do primeiro sintoma. Um checklist de cinco minutos teria poupado a sessão de depuração mais longa do projeto.
  • Testar de onde a produção chama, mais cedo. Do host o runtime era alcançável; do executor não era. Só o segundo teste descrevia a realidade, e eu o rodei tarde.
  • Desconfiar da camada que reporta o erro. Duas vezes a mensagem acusou um componente que estava funcionando bem — uma vez o Site, outra o DNS.
  • Escrever o runbook durante, não depois. Metade do valor deste projeto é saber refazê-lo, e isso quase evaporou.