Add guided Ubuntu production installer

This commit is contained in:
betoarts
2026-09-17 10:48:07 -03:00
parent 22b6547dc8
commit c3c99696b6
6 changed files with 226 additions and 4 deletions
+10
View File
@@ -0,0 +1,10 @@
---
impacto: capacidade_nova
secao: adicionado
titulo: Ubuntu ganha uma entrada guiada para instalação de produção
---
Uma VPS Ubuntu agora pode preparar as dependências e iniciar a instalação
completa de produção por um script na raiz. A entrevista pergunta o domínio,
gera o `.env` e os segredos, configura a comunicação interna dos serviços
Docker, escolhe Caddy ou o Traefik existente e só conclui após as verificações.
+7 -2
View File
@@ -132,11 +132,16 @@ Já dentro da VPS:
```bash
git clone https://github.com/melgarafael/DeskcommCRM.git
cd DeskcommCRM
bash hostgator-setup-kit/install.sh
bash ubuntu-production-installer.sh
```
É isso. **Você não instala Node, nem pnpm, nem compila nada** — a imagem do app já vem pronta.
Se faltar Docker, o instalador pergunta e instala sozinho.
O instalador prepara o Ubuntu, pergunta o domínio e as credenciais necessárias, gera o `.env`
e os segredos, configura a comunicação interna dos containers e escolhe HTTPS via Caddy ou
o Traefik que já existir na hospedagem. Se faltar Docker, ele pergunta e instala sozinho.
Em outra distribuição Linux, ou se você já preparou o servidor manualmente, a entrada canônica
continua disponível em `bash hostgator-setup-kit/install.sh`.
### O que você precisa ter em mãos
+3 -1
View File
@@ -1471,7 +1471,7 @@ chegou_na_deteccao() {
# printf '%s' "${RESTO_DAS_PERGUNTAS%%c*}" | grep -c ''
# → a quantidade depende do questionário atual do instalador. O último `c`
# confirma o aviso de DNS.
RESTO_DAS_PERGUNTAS="$(for _ in {1..17}; do printf '\n'; done; printf 'c\n')"
RESTO_DAS_PERGUNTAS="$(for _ in {1..19}; do printf '\n'; done; printf 'c\n')"
# A posição da cor DENTRO da fila acima — SUPABASE_ACCESS_TOKEN + 1 provedor +
# APP_IMAGE + OPENAI + APP_NAME + APP_LOCALE e ela é a 7ª. Fica numa variável porque a fila com a cor RESPONDIDA
@@ -1962,6 +1962,8 @@ STUB
fi
if ! grep -qx 'TRAEFIK_NETWORK="crmhost_teste_proxy"' "$PROJ/.env"; then
printf ' ✗ TRAEFIK_NETWORK errado no .env: %s\n' "$(grep -E '^TRAEFIK_NETWORK=' "$PROJ/.env" || echo '(ausente)')"
printf ' últimas linhas do instalador:\n'
printf '%s\n' "$saida" | tail -20 | sed 's/^/ /'
exit 1
fi
printf ' ✓ confirmando "s": cria a bridge e grava a rede com o nome que o compose usa\n'
+1 -1
View File
@@ -24,7 +24,7 @@
"test:journeys": "playwright test -c tests/journeys/playwright.config.ts",
"test:unit": "vitest run",
"test:db": "bash scripts/test-db.sh",
"test:shell": "bash tests/shell/update-guard.test.sh && bash tests/shell/dono-do-projeto.test.sh && bash tests/shell/scheduler-entrypoint.test.sh && bash tests/shell/owner-id-por-email.test.sh && bash hostgator-setup-kit/test-validators.sh",
"test:shell": "bash tests/shell/ubuntu-production-installer.test.sh && bash tests/shell/update-guard.test.sh && bash tests/shell/dono-do-projeto.test.sh && bash tests/shell/scheduler-entrypoint.test.sh && bash tests/shell/owner-id-por-email.test.sh && bash hostgator-setup-kit/test-validators.sh",
"test:invariants": "bash scripts/test-db.sh",
"lint:channels": "tsx scripts/lint-channels.ts",
"lint:role-rank": "tsx scripts/lint-role-rank.ts",
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Testes sem efeitos colaterais para a porta de entrada de produção.
set -uo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
SCRIPT="$ROOT_DIR/ubuntu-production-installer.sh"
FAILS=0
check() {
local descricao="$1"
shift
if "$@"; then
printf ' ✓ %s\n' "$descricao"
else
printf ' ✗ %s\n' "$descricao"
FAILS=$((FAILS + 1))
fi
}
echo "instalador Ubuntu de produção"
check "sintaxe Bash válida" bash -n "$SCRIPT"
ajuda="$(bash "$SCRIPT" --help 2>&1)"
check "--help termina sem preparar o host" test "$?" -eq 0
check "ajuda explica a entrevista de domínio" grep -q "pergunta o domínio" <<<"$ajuda"
check "ajuda distingue os serviços locais" grep -q "Serviços locais em Docker" <<<"$ajuda"
check "ajuda explica que --yes exige .env" grep -q "exige um .env previamente preenchido" <<<"$ajuda"
saida_invalida="$(bash "$SCRIPT" --desconhecido 2>&1)"
rc_invalido=$?
check "opção desconhecida é recusada" test "$rc_invalido" -eq 2
check "recusa mostra o uso" grep -q "Uso: bash ubuntu-production-installer.sh" <<<"$saida_invalida"
check "delega ao instalador self-host canônico" \
grep -q 'hostgator-setup-kit/install.sh' "$SCRIPT"
check "não mantém uma segunda lista de variáveis do .env" \
bash -c '! grep -qE "^(DOMAIN|SUPABASE_DB_URL|WAHA_API_KEY|INTERNAL_SECRET)=" "$1"' _ "$SCRIPT"
if [[ "$FAILS" -ne 0 ]]; then
printf '\n%d teste(s) falharam.\n' "$FAILS"
exit 1
fi
printf '\nTodos os testes do instalador Ubuntu passaram.\n'
+160
View File
@@ -0,0 +1,160 @@
#!/usr/bin/env bash
#
# Porta de entrada para instalar o DeskcommCRM em produção numa VPS Ubuntu.
# A entrevista, a geração do .env e a orquestração da stack continuam no
# instalador self-host canônico para que haja uma única fonte de verdade.
set -Eeuo pipefail
readonly ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly KIT_INSTALLER="$ROOT_DIR/hostgator-setup-kit/install.sh"
COLOR=0
if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then COLOR=1; fi
paint() {
local code="$1"
shift
if [[ "$COLOR" == 1 ]]; then
printf '\033[%sm%s\033[0m\n' "$code" "$*"
else
printf '%s\n' "$*"
fi
}
step() { printf '\n'; paint 32 "▶ $*"; }
warn() { paint 33 "⚠ $*"; }
die() { paint 31 "✖ $*" >&2; exit 1; }
usage() {
cat <<'EOF'
Uso: bash ubuntu-production-installer.sh [--yes]
Prepara uma VPS Ubuntu e inicia a instalação de produção do DeskcommCRM.
Modo interativo (recomendado):
bash ubuntu-production-installer.sh
O instalador pergunta o domínio, e-mail do certificado, Supabase, provedor de
IA, usuário administrador e opções de marca. Ele gera os segredos e o .env,
detecta Caddy/Traefik, sobe os serviços Docker e valida DNS, banco e saúde.
Modo não interativo:
bash ubuntu-production-installer.sh --yes
O modo --yes exige um .env previamente preenchido. Ele não inventa respostas
para credenciais nem escolhe silenciosamente o proxy de uma VPS ambígua.
Serviços locais em Docker:
app, worker, scheduler, WAHA, Redis, adaptador Redis HTTP e Caddy quando a
VPS não possui proxy reverso próprio. O Supabase pode ser provisionado pelo
instalador ou apontar para uma instalação Supabase própria com HTTPS.
EOF
}
confirmar() {
local resposta
read -r -p "$1 [S/n] " resposta
case "${resposta:-S}" in
[Nn]|[Nn][AaÃã][Oo]) return 1 ;;
*) return 0 ;;
esac
}
main() {
local -a argumentos_kit=()
local noninteractive=0
case "${1:-}" in
"") ;;
--yes)
argumentos_kit=(--yes)
noninteractive=1
;;
-h|--help)
usage
return 0
;;
*)
usage >&2
return 2
;;
esac
[[ $# -le 1 ]] || { usage >&2; return 2; }
[[ -r /etc/os-release ]] || die "Não consegui identificar o sistema operacional. Este instalador requer Ubuntu."
# shellcheck disable=SC1091
. /etc/os-release
[[ "${ID:-}" == "ubuntu" || "${ID_LIKE:-}" == *ubuntu* || "${ID_LIKE:-}" == *debian* ]] \
|| die "Distribuição não suportada (${PRETTY_NAME:-desconhecida}). Em outro Linux, use diretamente hostgator-setup-kit/install.sh."
case "$(uname -m)" in
x86_64|amd64) ;;
*) die "Arquitetura $(uname -m) não suportada pelas imagens de produção publicadas (linux/amd64)." ;;
esac
[[ -f "$KIT_INSTALLER" ]] || die "Instalador canônico não encontrado em $KIT_INSTALLER. Execute este arquivo dentro do repositório completo."
[[ -f "$ROOT_DIR/docker-compose.prod.yml" ]] || die "docker-compose.prod.yml não encontrado na raiz do projeto."
local -a privilegiado=()
if [[ "${EUID}" -ne 0 ]]; then
command -v sudo >/dev/null 2>&1 || die "Execute como root ou instale sudo para preparar o Ubuntu."
sudo -v || die "Não consegui obter permissão administrativa com sudo."
privilegiado=(sudo)
fi
step "Preparando dependências básicas do Ubuntu"
if ! "${privilegiado[@]}" apt-get update; then
warn "Um repositório APT externo falhou; tentarei usar os índices válidos já disponíveis."
fi
"${privilegiado[@]}" apt-get install -y ca-certificates curl git openssl
local docker_instalado_agora=0
if ! command -v docker >/dev/null 2>&1; then
warn "Docker ainda não está instalado."
if [[ "$noninteractive" == 0 ]] && ! confirmar "Posso instalar o Docker usando o instalador oficial?"; then
die "Sem Docker não é possível subir a aplicação."
fi
step "Instalando Docker Engine e Docker Compose"
local instalador_docker
instalador_docker="$(mktemp)"
trap 'rm -f "${instalador_docker:-}"' RETURN
curl -fsSL https://get.docker.com -o "$instalador_docker"
"${privilegiado[@]}" sh "$instalador_docker"
rm -f "$instalador_docker"
trap - RETURN
docker_instalado_agora=1
fi
command -v docker >/dev/null 2>&1 || die "Docker foi instalado, mas não apareceu no PATH."
# O daemon pode estar disponível só para root. Nesse caso toda a entrevista
# roda com sudo, mantendo o .env e os crons sob o mesmo dono da instalação.
local -a executor=(bash)
if docker info >/dev/null 2>&1; then
docker compose version >/dev/null 2>&1 || die "Docker Compose v2 não está disponível."
elif [[ "${EUID}" -ne 0 ]] && sudo docker info >/dev/null 2>&1; then
sudo docker compose version >/dev/null 2>&1 || die "Docker Compose v2 não está disponível para root."
executor=(sudo -E bash)
if [[ "$docker_instalado_agora" == 1 ]]; then
warn "O grupo docker só vale após novo login; esta primeira instalação continuará com sudo."
else
warn "Seu usuário não acessa o daemon Docker; a instalação continuará com sudo."
fi
else
die "O daemon Docker não está rodando ou não pode ser acessado."
fi
step "Iniciando a instalação inteligente de produção"
printf '%s\n' " • o domínio será perguntado e validado;"
printf '%s\n' " • o .env será gerado com permissão 600 e segredos aleatórios;"
printf '%s\n' " • app, worker, scheduler, WAHA e Redis conversarão pela rede Docker interna;"
printf '%s\n' " • Caddy emitirá HTTPS ou o proxy Traefik existente será detectado;"
printf '%s\n' " • a conclusão depende de banco, containers, rota de saúde e domínio responderem."
cd "$ROOT_DIR"
exec "${executor[@]}" "$KIT_INSTALLER" "${argumentos_kit[@]}"
}
main "$@"