Pular para o conteúdo principal

Bootstrap do servidor

Primeira instalação do SmartPerformance SFA num servidor zerado: bootstrap.sh. Decisões em ADR-045 (.claude/docs/decisions.md).

Qual script usar
SituaçãoScript
Servidor novo, sem .env, sem nada em 80/443bootstrap.sh
Instalação que já existe, subindo uma versão novadeploy.sh

O bootstrap.sh é idempotente: rodar de novo não regenera segredo já definido nem reemite certificado válido. É para ser rodado outra vez depois de corrigir um DNS ou liberar a porta 80.

Pré-requisitos​

  • Docker Engine + plugin docker compose (v2) e openssl;
  • portas 80 e 443 livres no host e liberadas no firewall da nuvem;
  • registros A dos três domínios apontando para o servidor.

Uso​

git clone <repo> && cd SmartPerformanceSfa

bash bootstrap.sh --email voce@empresa.com \
--app sfa.exemplo.com \
--api api.sfa.exemplo.com \
--docs docs.sfa.exemplo.com

Você não precisa editar o .env antes. A precedência é argumento → .env → pergunta: sem argumentos ele pergunta o que faltar e grava no .env; na segunda execução lê tudo de lá e não pergunta nada. Valor de exemplo (*.exemplo.com, change_me…) conta como não preenchido — com um domínio de exemplo herdado, o certbot tentaria provar um domínio que não é seu.

Sem terminal para perguntar (ssh host 'bash bootstrap.sh', CI), passe tudo por argumento — o script avisa em vez de falhar no read.

OpçãoPara quê
--stagingEnsaio com o ambiente de teste do Let's Encrypt — não gasta o limite de emissões.
--sem-tlsRede interna/homologação sem domínio público: HTTP puro, sem certificado.
--sem-buildPula o build das imagens (já construídas ou vindas do registry).

O que ele faz​

  1. Pré-requisitos — Docker, compose v2, openssl; avisa se 80/443 já estão ocupadas.
  2. .env — cria a partir do .env.example e gera os segredos: POSTGRES_PASSWORD, JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, INTEGRATION_SECRET_KEY. Grava NODE_ENV=production, BIND_ADDR=127.0.0.1 e as URLs internas do compose (postgres, redis, api).
  3. Domínios — grava DOMAIN_*, WEB_ORIGIN e NEXT_PUBLIC_DOCS_URL; confere o DNS (aviso, não erro — atrás de CDN o A record não é o IP do servidor).
  4. Stack — build, sobe postgres/redis, espera o banco e sobe api/web/docs. As migrations e o seed rodam no próprio CMD da imagem da API.
  5. nginx (fase 1) — só a porta 80, servindo o desafio ACME.
  6. certbot — um certificado com os três nomes.
  7. nginx (fase 2) — vhosts TLS, HTTP passa a redirecionar, e a renovação automática sobe.
  8. Conferência — curl nos três endereços.

Por que duas fases​

Um vhost TLS apontando para um certificado que ainda não existe impede o nginx de subir; e sem nginx no ar o certbot não tem como provar o domínio. A fase 1 quebra esse ciclo servindo apenas /.well-known/acme-challenge/ em HTTP.

Arquivos​

bootstrap.sh o script
deploy/lib/bootstrap.sh biblioteca compartilhada (cópia — ver deploy/lib/README.md)
docker-compose.prod.yml nginx + certbot (some ao docker-compose.yml)
deploy/nginx/templates/ http.conf · tls.conf · plain.conf (com campos a substituir)
deploy/nginx/includes/ proxy.inc + um site-*.inc por host
deploy/nginx/conf.d/ RENDERIZADO — fora do git

A biblioteca vem da skill server-bootstrap do project-template e carrega o que não vale a pena redescobrir a cada projeto: .env como memória (com escape de & no sed), render que recusa configuração com campo por substituir, --keep-until-expiring no certbot, conferência HTTP que não mente. Como aqui a borda roda em container, o script redefine os ganchos certbot_cli, nginx_test e nginx_reload — o padrão da biblioteca é falar com o nginx e o certbot do host.

Os includes guardam o que é específico de cada host: limite de corpo de 25 MB (molde da organização, lotes de integração) e SSE sem buffer para a tela de Execuções — com buffer, o "ao vivo" atualiza em rajadas ou nunca.

Depois do bootstrap​

  • Troque a senha do admin pela tela (o script exibe a gerada uma única vez).
  • Guarde o .env fora do servidor: ele contém a chave que decifra as credenciais de integração. Perdê-la significa recadastrar todas.
  • Atualizações seguintes: bash deploy.sh --tag <versão> (abaixo).

Atualizar: deploy.sh (ADR-058)​

O servidor não constrói mais o projeto. Ele baixa do registry privado a imagem que o CI publicou — a mesma que rodou em homologação — identificada por uma tag imutável.

bash deploy.sh --tag 1a2b3c4 # sobe essa versão
bash deploy.sh # reusa o IMAGE_TAG gravado no .env
bash deploy.sh --tag <anterior> # rollback: a tag antiga continua no registry
OpçãoPara quê
--tag <tag>Versão a subir: sha curto do commit ou semver. Fica gravada no .env.
--skip-gitNão atualiza o repositório (dele vêm compose, nginx e templates — não a aplicação).
--skip-migrateNão roda prisma migrate deploy.
--skip-docsDeixa o serviço de docs de fora.
--seedRoda o seed (idempotente) depois das migrations.
--build-localEmergência: constrói no servidor, quando o registry está fora.

A ordem existe por um motivo: a tag é validada antes de qualquer efeito, as imagens são baixadas antes de mexer no que está no ar (falha de download não derruba a versão anterior), as migrations rodam a partir da imagem que vai subir, e o up usa --no-build — sem isso, imagem ausente viraria um build silencioso no servidor, que é justamente o que se quer eliminar.

Rollback é trocar a tag. Como a tag é imutável, subir a anterior devolve exatamente os bytes que estavam rodando antes, em segundos.

Tag móvel é recusada

latest, prod, main, homolog e dev não são aceitas. Com um ponteiro móvel, ninguém sabe o que está no ar e qualquer restart da VPS vira um deploy não intencional.

Credencial do servidor

O servidor só baixa: use um token de teto reader, nunca o token do CI (que publica). Rode docker login <REGISTRY_HOST> uma vez na máquina — assim o token não passa por arquivo. Para execução não-interativa (cron), o script aceita REGISTRY_USERNAME/REGISTRY_TOKEN do ambiente.

Versão, ponteiro e promoção​

Duas espécies de tag convivem no registry, e confundi-las é a origem de quase todo susto de deploy:

EspécieExemplosPapelQuem cria
Identidade (imutável)1a2b3c4, 0.1.0qual artefato é esteo push: CI a cada commit na main, ou scripts/publish-images.sh (que usa o sha do HEAD)
Ponteiro (móvel)homolog, prodqual artefato está aprovado naquele ambientea promoção: workflow docker-promote ou scripts/promote-image.sh

O ponteiro não carrega bytes: ele é um apelido para o digest de uma identidade. Nada no Docker sabe o que "prod" significa — é texto livre, e o sentido só existe porque está escrito aqui.

./scripts/publish-images.sh # publica as três com a tag do commit (HEAD)
./scripts/promote-image.sh 1a2b3c4 homolog # marca como aprovado em homologação
./scripts/promote-image.sh 1a2b3c4 prod # marca como aprovado em produção
bash deploy.sh --tag 1a2b3c4 # aplica no servidor
A tag não se digita

publish-images.sh não pergunta a versão: a tag é o sha curto do HEAD. Uma versão digitada pode mentir sobre o que está dentro da imagem — basta errar o número, ou publicar "0.1.0" de um checkout que não é o 0.1.0. Derivada do commit, ela é verificável com git show. Por isso o script também recusa árvore suja: com alteração não commitada, a imagem não conteria o commit cujo nome ela levaria (--allow-dirty publica como <sha>-dirty, que serve para depurar e não pode ser promovida).

Semver marcada continua existindo, mas nasce de um objeto do Git criado de propósito: o CI publica 1.2.3 ao receber a tag git v1.2.3.

Promover não é deployar. A promoção só cria o apelido no registry; quem troca o que está rodando é o deploy.sh, e ele referencia a identidade. Dá para marcar "aprovado" numa sexta e aplicar na segunda — e é por isso que produção nunca aponta para prod.

O script recusa o que quebraria a garantia: origem que não seja imutável (promover a partir de homolog copiaria o que ele estiver apontando naquele instante) e destino que pareça versão (reapontar 1.2.3 é republicar). Confere que todas as imagens existem na origem antes de mover qualquer uma — promover metade deixaria api e web de commits diferentes — e imprime o digest anterior do ponteiro, com o comando pronto para desfazer.

Se um dia produção apontar para prod

Enquanto o servidor referencia a identidade, mover o ponteiro é trocar um rótulo — reversível. No dia em que o compose de produção passar a apontar para prod, promover vira deploy, e aí a operação deve sair da máquina de qualquer um e valer só pelo CI, com aprovação registrada.

O bootstrap.sh ainda constrói

A primeira instalação continua buildando no servidor. Um servidor novo, portanto, nasce com imagens locais e só passa a rodar artefato publicado no primeiro deploy.sh. Use --sem-build se as imagens já tiverem sido baixadas.

Quando der errado​

SintomaCausa provável
certbot falha na validaçãoDNS ainda não propagou, porta 80 fechada no firewall da nuvem, ou limite de emissões. Corrija e rode de novo; use --staging para ensaiar.
port is already allocatednginx do sistema ocupando 80/443 — a borda aqui roda em container (systemctl stop nginx).
host not found in upstreamUm dos containers de aplicação não subiu: docker compose logs api web docs.
Página carrega em HTTP mas não em HTTPSFase 2 não rodou (o certbot falhou antes). O .env e o resto estão intactos: rode o bootstrap de novo.
Tela de Execuções não atualiza sozinhaAlgum proxy à frente (CDN) bufferizando o SSE.