Installation
Instalar num Servidor Público Ubuntu
Coloca o QuoteNode num servidor público Ubuntu com domínio, HTTPS, SMTP, backups e fundamentos de firewall.
Instalar num Servidor Público Ubuntu
Usa este caminho para um deployment em produção acessível por clientes através de um domínio público.
Quando usar
- Clientes externos devem abrir links públicos de propostas.
- As mensagens de email devem conter links funcionais.
- Queres controlo direto sobre o servidor em vez de uma camada PaaS.
O que não vai funcionar sem configuração adequada
- Links públicos falham se o DNS não apontar para o servidor.
- Links públicos falham se o URL do browser diferir da configuração CORS.
- A emissão do certificado HTTPS falha se os portos 80/443 estiverem bloqueados.
- Links de email falham se o SMTP estiver em falta ou se as mensagens forem rejeitadas pelo provider de email do destinatário.
- Deployments com IP doméstico dinâmico precisam de DDNS e port forwarding no router; um VPS é normalmente mais simples.
Pré-requisitos
- Servidor Ubuntu LTS ou VPS.
- Registo DNS público, por exemplo
crm.example.com, a apontar para o servidor. - Portos 80 e 443 abertos a partir da internet.
- Docker Engine 24.0+ e Docker Compose v2.
- Conta SMTP para entrega de email.
- Pelo menos 4 GB de RAM e 20 GB de espaço livre em disco (recomendados para produção).
- Destino de backup e um procedimento de restore.
Verifica o Docker:
docker --version # Should show 24.0+
docker compose version # Should show v2.x
Passo 1 — Criar uma diretoria de projeto
mkdir quotenode && cd quotenode
Passo 2 — Criar o ficheiro de configuração
Cria um ficheiro chamado .env. Substitui crm.example.com pelo teu domínio real:
# Database
DB_URL=jdbc:postgresql://postgres:5432/quotenode
DB_USERNAME=quotenode
DB_PASSWORD=change-me-to-something-random-32-chars
DB_NAME=quotenode
# ============================================================
# SECURITY SECRETS — UNIQUE to this installation.
# SAVE THIS FILE and keep a secure backup.
# ============================================================
DB_ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
TIMING_TOKEN_SECRET=change-me-timing-secret-min-32-chars
PUBLIC_LINK_PASSWORD_SESSION_SECRET=change-me-session-secret-min-32
# Public domain — replace with your real domain
CORS_ALLOWED_ORIGINS=https://crm.example.com
DOMAIN=crm.example.com
# Email delivery
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME[email protected]
SMTP_PASSWORD=your-smtp-password
SMTP_AUTH=true
SMTP_STARTTLS=true
# Notification preference links (must match your public domain)
NOTIFICATIONS_PUBLIC_PREFERENCES_URL=https://crm.example.com/notifications/preferences
# O IP do cliente é detetado automaticamente atrás de proxies internos e CDNs (incluindo Cloudflare) — deixe vazio.
# Defina apenas para um proxy invulgar cujo par de transporte seja um endereço PÚBLICO a confiar (IP/CIDR ou nome de host).
SECURITY_TRUSTED_PROXIES=
# Application
LOG_LEVEL=INFO
SPRING_PROFILES_ACTIVE=prod
Faz backup do teu ficheiro
.envantes do primeiro uso em produção. Guarda-o num gestor de passwords ou num cofre físico. ODB_ENCRYPTION_KEYé crítico — perdê-lo significa que os dados cifrados ficam perdidos para sempre.
Alternativa SMTP: Podes omitir as variáveis SMTP_* do .env e configurar o email mais tarde em Definições > SMTP de email no QuoteNode. O painel de administração armazena a password SMTP cifrada com DB_ENCRYPTION_KEY, mantendo-a fora do teu ficheiro .env.
Passo 3 — Criar o ficheiro Docker Compose
Cria um ficheiro chamado docker-compose.yml:
services:
postgres:
image: postgres:18-alpine
cpus: 4.0
mem_limit: 512m
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
PGDATA: /var/lib/postgresql/data # required for the postgres:18 image
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME}"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
gotenberg:
image: gotenberg/gotenberg:8
cpus: 2.0
mem_limit: 448m
environment:
LOG_LEVEL: info
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
backend:
image: ghcr.io/lesisty7/quotenode/quotenode-api:latest
cpus: 4.0
mem_limit: 1792m
depends_on:
postgres: { condition: service_healthy }
gotenberg: { condition: service_healthy }
env_file: .env
environment:
SPRING_PROFILES_ACTIVE: prod
JOBS_MODE: web
PDF_ENABLED: "true"
PDF_GOTENBERG_URL: http://gotenberg:3000
volumes:
- backend_uploads:/app/data/uploads
- backend_pdfs:/app/data/pdfs
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
interval: 15s
timeout: 10s
retries: 5
start_period: 45s
restart: unless-stopped
frontend:
image: ghcr.io/lesisty7/quotenode/quotenode-frontend:latest
cpus: 2.0
mem_limit: 192m
depends_on:
backend: { condition: service_healthy }
ports:
- "80:80"
- "443:443"
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:80/"]
interval: 15s
timeout: 5s
retries: 3
restart: unless-stopped
volumes:
postgres_data:
backend_uploads:
backend_pdfs:
Passo 4 — Configurar o firewall
Abre apenas o necessário:
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
Não expões o PostgreSQL publicamente. Fica na rede Docker.
Passo 5 — Iniciar o QuoteNode
docker compose up -d
Acompanha o arranque:
docker compose logs -f backend
Aguarda até ver Started QuoteNodeApplication. O container frontend (Caddy) obterá automaticamente um certificado Let’s Encrypt para o teu domínio.
Passo 6 — Abrir o QuoteNode
Navega para https://crm.example.com no teu browser.
Credenciais de acesso predefinidas:
| Campo | Valor |
|---|---|
[email protected] | |
| Password | Admin123! |
Altera a password predefinida imediatamente após iniciar sessão.
Verificações pós-deployment
- Iniciar sessão e alterar a password admin.
- Criar uma proposta de teste.
- Gerar um PDF.
- Criar e abrir um link público a partir de outro dispositivo.
- Se o SMTP estiver configurado, enviar uma notificação de teste e verificar que os links do email funcionam.
O que está a correr?
| Container | O que faz | Uso de RAM |
|---|---|---|
postgres | Armazena todos os teus dados | ~256 MB |
backend | Servidor API Java, lógica de negócio | ~512 MB – 1,5 GB |
gotenberg | Converte HTML para PDF (baseado em Chromium) | ~200 MB |
frontend | Serve a interface web + reverse proxy (Caddy com auto-HTTPS) | ~20 MB |
Total: aproximadamente 1,5 – 2,5 GB de RAM.
Comandos habituais
# Check service status
docker compose ps
# View logs
docker compose logs -f backend
# Stop QuoteNode
docker compose down
# Update to the latest version
docker compose pull && docker compose up -d
Checklist de upgrade
Antes de cada upgrade:
- Confirma a versão atual da app na área de administração.
- Faz backup do
.env. - Corre ou verifica um backup fresco da base de dados.
- Faz pull das novas imagens e reinicia.
- Verifica health do backend, login, geração de PDF e um link público de proposta.
Manter os teus segredos seguros
| Segredo | O que protege | Se perdido |
|---|---|---|
DB_PASSWORD | Acesso à base de dados | Recuperável — reiniciar via ferramentas de administração PostgreSQL |
DB_ENCRYPTION_KEY | Códigos MFA, password SMTP, chave API FX, campos PII | Permanentemente ilegível |
TIMING_TOKEN_SECRET | Tokens anti-timing-attack | Regenerar — utilizadores re-autenticam-se |
PUBLIC_LINK_PASSWORD_SESSION_SECRET | Sessões de links públicos | Regenerar — as sessões ativas expiram |
Guarda o
.envnum gestor de passwords (1Password, Bitwarden). Para máxima segurança, imprime os segredos e guarda a impressão numa caixa-forte física. Nunca faças commit do.envno controlo de versão.
Recuperação se .env for perdido
Se perderes o teu ficheiro .env mas tiveres uma cópia de segurança da base de dados não cifrada (o modo padrão):
- Cria um novo
.envcom novos segredos. - Restaura a base de dados a partir da tua cópia de segurança.
- Todos os dados de negócio serão acessíveis.
- Perdidos: códigos MFA (utilizadores re-inscrevem-se), password SMTP (reinserir), chave API FX (reinserir). Se
ENCRYPT_PII=trueestava ativo, os dados de clientes cifrados também são perdidos.