Installation
Atualizar o QuoteNode
Como verificar novas versões, atualizar para o pacote de uma nova versão e reverter com segurança.
Atualizar o QuoteNode
O QuoteNode verifica automaticamente se existem novas versões. Quando há uma versão mais recente, o painel de administração mostra uma notificação com a etiqueta da versão e uma ligação para as notas de versão.
Cada versão é publicada como pacote de implementação: docker-compose.yml (e docker-compose.coolify.yml) com as imagens da versão fixadas por digest, env.example, release.json e as ferramentas de operator/. Atualizar consiste em substituir o ficheiro Compose pelo do pacote seguinte. A versão atual é v0.9.8-alpha (pacote).
A forma de atualizar depende de como o QuoteNode foi instalado. A lista e os comandos abaixo destinam-se a um diretório de instalação (servidor, LAN, portátil ou uma pasta num NAS com docker-compose.yml e .env). Uma stack do Portainer não tem esse diretório: segue para Stack do Portainer. Coolify: segue para Coolify.
Problema conhecido na v0.9.5-alpha: destino de cópia de exemplo
O env.example da v0.9.5-alpha contém uma linha de exemplo ativa, BACKUP_RCLONE_REMOTE=s3:mybucket/backups. Esse destino não está configurado: se a linha chegar às tuas definições, todas as cópias de segurança falham e um envio falhado não deixa nenhuma cópia local dessa cópia. As definições destes guias nunca a contêm; verifica a tua instalação se criaste o .env a partir do env.example ou lhe acrescentaste definições desse ficheiro durante uma atualização.
Num diretório de instalação, comenta exatamente essa linha de exemplo (um destino real mantém-se inalterado) e recria os contentores:
# Run in Bash (for example Linux, Git Bash or WSL); other shells can edit the exact placeholder manually.
(
set -eu
umask 077
temporary_env="$(mktemp .env.backup-remote.XXXXXX)"
trap 'rm -f "$temporary_env"' EXIT
carriage_return="$(printf '\r')"
# Match only the published placeholder, with either LF or Windows CRLF line endings.
sample_pattern="^BACKUP_RCLONE_REMOTE=s3:mybucket/backups${carriage_return}\{0,1\}$"
sed "s|${sample_pattern}|# &|" .env > "$temporary_env"
if LC_ALL=C grep -Eq "^BACKUP_RCLONE_REMOTE=s3:mybucket/backups${carriage_return}?$" "$temporary_env"; then
echo "Sample backup remote is still active; settings were not replaced." >&2
exit 1
else
match_status=$?
[ "$match_status" -eq 1 ] || exit "$match_status"
fi
# Keep real remote settings unchanged; publish a changed file atomically with private permissions.
if ! cmp -s .env "$temporary_env"; then
mv "$temporary_env" .env
fi
docker compose up -d
)
No Portainer, apaga a variável da stack BACKUP_RCLONE_REMOTE se tiver esse valor de exemplo e depois atualiza a stack. A definição Coolify da v0.9.5-alpha não passa esta variável à aplicação; apaga o valor de exemplo do recurso antes de atualizar para uma versão posterior.
As cópias ficam então no volume de cópias de segurança. Inicia uma cópia no painel de administração (Admin → Security → Backup), confirma que termina com sucesso e garante uma cópia fora do servidor: um destino rclone configurado ou cópias regulares dos arquivos para outra máquina (Cópias de segurança e recuperação).
Passar para o PostgreSQL 18
Os ficheiros Compose já usam postgres:18-alpine, por isso as instalações novas não precisam de nada. Se uma versão alfa anterior corria no PostgreSQL 16, migra os dados uma vez — um servidor PostgreSQL 18 não arranca sobre um diretório de dados mais antigo, e a aplicação nunca o reescreve automaticamente. Segue o procedimento dedicado e seguro: Migrar os dados para o PostgreSQL 18 (por linha de comandos ou apenas pela interface, por plataforma).
Como funciona a verificação de versões
A interface de administração obtém https://quotenode.dev/releases/latest.json quando um administrador inicia sessão. É um pedido GET só de leitura e sem autenticação; nenhum dado da tua instância é enviado. Se o manifesto estiver inacessível, a verificação falha em silêncio e o painel funciona normalmente.
A verificação e o URL do manifesto fazem parte da interface web já construída. Definir VITE_RELEASE_CHECK_ENABLED ou VITE_RELEASE_MANIFEST_URL no .env ou no Coolify não tem efeito numa versão; esses valores só se aplicam se construíres tu mesmo a imagem do frontend a partir do código-fonte. Para que uma instalação não contacte quotenode.dev, bloqueia esse endereço na tua rede.
Lista de verificação antes da atualização
Para um diretório de instalação; o Portainer e o Coolify têm secções próprias mais abaixo.
- Regista a versão atual:
release.jsonno diretório de instalação, ou Admin > Licença. - Copia o
.envpara fora do servidor — contém segredos que não podem ser regenerados (sobretudoDB_ENCRYPTION_KEY). - Transfere o novo pacote para junto da instalação. Isto não altera nada do que está em execução:
mkdir -p ../next && cd ../next
curl -fsSLO https://quotenode.dev/releases/v0.9.8-alpha/quotenode-deploy-v0.9.8-alpha.tar.gz
tar -xzf quotenode-deploy-v0.9.8-alpha.tar.gz --strip-components=1
cd -
# Definições acrescentadas ou removidas pela nova versão? Compara o teu .env com o novo modelo.
diff <(grep -o '^[A-Z_]*=' .env | sort) <(grep -o '^[A-Z_]*=' ../next/env.example | sort)
- Acrescenta ao
.envas definições de que a nova versão precisa (a comparação acima lista-as; instalações anteriores aos pacotes de versão: vê Instalações anteriores aos pacotes de versão). A instalação em execução ainda não as usa. - Cria o ponto de controlo da atualização e uma cópia de segurança verificada com a ferramenta do novo pacote. Isto não altera nada do que está em execução e para no primeiro erro:
BACKUP_ARCHIVE_ENCRYPTION_MODE=AGE_RECIPIENT BACKUP_AGE_RECIPIENT=age1... \
../next/operator/upgrade.sh prepare --export-to ../quotenode-backups
O ponto de controlo (em .quotenode-upgrade/, usado apenas por esta instalação) regista os docker-compose.yml, .env e release.json instalados, a identidade das imagens em execução e o estado do esquema da base de dados. A cópia é feita com as ferramentas da nova versão sobre o projeto Compose e os volumes desta instalação, fica local nesta execução mesmo que haja cópias remotas configuradas e só é guardada em ../quotenode-backups quando o seu SHA-256 coincide. Copia esse diretório para fora do servidor antes de continuar. O destinatário age está no teu Recovery Kit (Cópias de segurança e recuperação).
- Se as cópias agendadas estiverem ativas, verifica também se a última foi bem-sucedida (painel de administração).
Procedimento de atualização
Docker Compose (servidor, LAN, portátil, pasta num NAS)
Depois da lista de verificação, executa no diretório de instalação, para que o projeto Compose e os seus volumes se mantenham:
../next/operator/upgrade.sh apply
upgrade.sh apply transfere primeiro todas as imagens da nova versão e para no primeiro erro: se uma transferência falhar, a instalação continua a funcionar sem alterações. Só depois instala docker-compose.yml, release.json, env.example e operator/ do pacote, inicia a nova versão e espera até estar saudável. As migrações da base de dados correm automaticamente quando o novo backend arranca. É necessário o Docker Compose 2.17 ou mais recente.
Stack do Portainer
O upgrade.sh precisa de um diretório de instalação, por isso uma stack do Portainer atualiza-se no Portainer:
- Confirma que a última cópia agendada teve sucesso (painel de administração), protege os volumes da stack com a ferramenta de instantâneos ou de cópias da tua plataforma e guarda uma cópia da definição atual da stack e das suas variáveis de ambiente.
- Abre o Editor da stack e substitui a definição pelo bloco abaixo: a adaptação para o Portainer do
docker-compose.ymlda versão, o mesmo bloco de Instalar com o Portainer. Não coles o ficheiro original do pacote: lê.env, que o Portainer não tem. - Mantém o nome da stack e as suas variáveis de ambiente (
stack.env); acrescenta as definições de que a nova versão precisa (compara com o respetivoenv.example). - Clica em Update the stack com a obtenção de imagens ativa. As migrações da base de dados correm no arranque do novo backend.
# Portainer Web editor adaptation of the release docker-compose.yml: env_file reads stack.env (the stack's
# environment variables) instead of .env in postgres, backend and backup-worker. Images, services, ports and
# volumes are unchanged. For a folder-based installation use the unchanged file of the release package.
x-json-file-logging: &json-file-logging
driver: json-file
options:
max-size: "${DOCKER_LOG_MAX_SIZE:-10m}"
max-file: "${DOCKER_LOG_MAX_FILE:-5}"
# ==========================================================================
# QuoteNode — standalone deployment (docker compose)
# GENERATED by scripts/render-deployment.mjs — do not edit by hand.
# Release v0.9.8-alpha (commit ff74453ca68f3c40bd86a32f0b0b813aaffc7aea, build 28); platforms: linux/amd64.
# API image: ghcr.io/lesisty7/quotenode/quotenode-api@sha256:0ed701f952c27ea0e1db168a00de75facb231f7c6d5f6b4f0f3b77ef98d4f140
# Frontend image: ghcr.io/lesisty7/quotenode/quotenode-frontend@sha256:40a1cdc4fca5cebd9f43d34daef4ba059a88000ff08af821e199566f07260606
#
# Settings: the .env file in this directory (start from env.example, the commented catalogue of every
# setting; postgres, backend and backup-worker read it). Start: docker compose up -d
# DOMAIN: public host name for automatic HTTPS, or :80 / http://<address> for plain HTTP on a trusted LAN.
# Only the caddy entry point publishes ports (HTTP_PORT/HTTPS_PORT, default 80/443).
# Upgrades: replace this file with the next release package version; volumes keep the data.
# ==========================================================================
services:
postgres:
# Database (PostgreSQL 18); internal network only, data in the database volume.
image: postgres:18-alpine@sha256:77f585114c32fbca283dc835b0596f4e52b51b4c6662d7810b2f4084f60a1873
# JIT is off by default: for this OLTP workload most queries fall below jit_above_cost,
# so JIT compilation rarely helps and can add latency. Set PG_JIT=on to re-enable.
command: ["postgres", "-c", "jit=${PG_JIT:-off}"]
env_file: stack.env
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_INITDB_ARGS: "--data-checksums"
# postgres:18 refuses to start on a bare /var/lib/postgresql/data mount (new default layout).
# Pin PGDATA to the legacy path so the existing volume mount keeps working; major upgrades go
# through a logical dump/restore, never an in-place upgrade of the data directory.
PGDATA: /var/lib/postgresql/data
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d ${DB_NAME}"]
interval: 10s
timeout: 5s
retries: 10
# CPU ceilings are opt-in (*_CPUS): 0 = no ceiling, all host cores. Docker refuses a ceiling above the
# host's core count, so a fixed default would stop the stack on smaller hosts (minimum: 2 cores).
cpus: ${POSTGRES_CPUS:-0}
# Ceiling, not a reservation: PG sits at tens of MB until working set/connections grow.
# Default 512m fits the 4 GB-VM profile; raise POSTGRES_MEM_LIMIT for larger tiers.
mem_limit: ${POSTGRES_MEM_LIMIT:-512m}
restart: unless-stopped
logging: *json-file-logging
# No published host port: the database is reachable only on the internal network.
gotenberg:
# PDF rendering (Chromium) for the backend; internal network only, keeps no data.
image: gotenberg/gotenberg:8@sha256:aa552cc115db488e0f0bcb07446aac13071ad51505b642c84bb894500907b42b
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 5
cpus: ${GOTENBERG_CPUS:-0}
# Chromium PDF rendering: measured ~188 MiB peak rendering a real offer template, so
# 448m keeps ~2.4x headroom. Freed budget goes to backup-worker (closer to its limit).
mem_limit: ${GOTENBERG_MEM_LIMIT:-448m}
restart: unless-stopped
logging: *json-file-logging
backend:
# API server (Java): business logic and web jobs; uploads, PDFs and the GeoIP database live in volumes.
image: ghcr.io/lesisty7/quotenode/quotenode-api@sha256:0ed701f952c27ea0e1db168a00de75facb231f7c6d5f6b4f0f3b77ef98d4f140
# Time Docker gives the backend between SIGTERM and SIGKILL. 60 s = the SMTP handoff hard limit
# (mail.handoff.deadline-ms + end-of-data-allowance-ms = 25 s) + 5 s to record its outcome + the HTTP
# graceful drain phase (30 s). A ceiling, not a delay: an idle backend exits in seconds.
stop_grace_period: 60s
depends_on:
postgres:
condition: service_healthy
gotenberg:
condition: service_healthy
env_file: stack.env
environment:
SPRING_PROFILES_ACTIVE: prod
JOBS_MODE: web
# The caddy edge below is the backend's only HTTP peer, so it is the trusted proxy by default (security
# finding #80); without it every request would appear to come from the edge container. Override only
# for a custom proxy chain.
SECURITY_TRUSTED_PROXIES: ${SECURITY_TRUSTED_PROXIES:-caddy}
BACKUP_LOCAL_DIR: /app/data/backups
volumes:
- backend_uploads:/app/data/uploads
- backend_pdfs:/app/data/pdfs
- backend_backups:/app/data/backups
- backend_geoip:/app/data/geoip
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
interval: 15s
timeout: 10s
retries: 5
start_period: 45s
cpus: ${BACKEND_CPUS:-0}
# Heap auto-scales to 70% of this limit (MaxRAMPercentage); default 1536m → ~1 GB heap,
# fits the 4 GB-VM profile. Raise BACKEND_MEM_LIMIT for heavier load (see resource docs).
mem_limit: ${BACKEND_MEM_LIMIT:-1536m}
restart: unless-stopped
logging: *json-file-logging
backup-worker:
# Scheduled backups: same image as the backend, JOBS_MODE=backup-only; archives go to the backups volume.
image: ghcr.io/lesisty7/quotenode/quotenode-api@sha256:0ed701f952c27ea0e1db168a00de75facb231f7c6d5f6b4f0f3b77ef98d4f140
depends_on:
postgres:
condition: service_healthy
backend:
condition: service_healthy
env_file: stack.env
environment:
SPRING_PROFILES_ACTIVE: prod
JOBS_MODE: backup-only
BACKUP_LOCAL_DIR: /app/data/backups
# Worker runs pg_dump in a shell (out-of-JVM) and never does web/import heap work,
# so it needs little heap. On this small 768m container the ~350 MB fixed non-heap
# cost makes the global MaxRAMPercentage=70 (538 MB heap → 888 MB total) unsafe.
# Cap heap lower so heap_max + non-heap stays well under the limit.
JAVA_OPTS: "-XX:MaxRAMPercentage=45.0 -XX:+UseG1GC -XX:G1PeriodicGCInterval=300000"
volumes:
- backend_uploads:/app/data/uploads
- backend_pdfs:/app/data/pdfs
- backend_backups:/app/data/backups
- backend_geoip:/app/data/geoip
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
interval: 15s
timeout: 10s
retries: 5
# JVM boots a large Spring context (CPU-bound); the grace window must outlast
# cold start to avoid a false-unhealthy restart loop under load.
start_period: 300s
# CPU is a ceiling (idle cost ~0); give the worker enough to boot fast and to run
# pg_dump + age-encryption/compression bursts without throttling. Idles near zero CPU.
cpus: ${WORKER_CPUS:-0}
# Default 832m: measured backup peak (JVM idle + pg_dump + tar + gzip) is ~607 MiB, so
# this keeps a ~27% margin. Do NOT set WORKER_MEM_LIMIT below ~768m or backups risk OOM.
mem_limit: ${WORKER_MEM_LIMIT:-832m}
restart: unless-stopped
logging: *json-file-logging
frontend:
# Web UI: the static build served by Caddy, reached through the caddy entry point.
image: ghcr.io/lesisty7/quotenode/quotenode-frontend@sha256:40a1cdc4fca5cebd9f43d34daef4ba059a88000ff08af821e199566f07260606
depends_on:
backend:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:80/"]
interval: 15s
timeout: 5s
retries: 3
cpus: ${FRONTEND_CPUS:-0}
# Prod frontend is Caddy serving the static build (measured ~46 MiB peak under load); 96m is ample.
# CPU is a free ceiling, so headroom for concurrent connections costs nothing at idle.
mem_limit: ${FRONTEND_MEM_LIMIT:-96m}
restart: unless-stopped
logging: *json-file-logging
caddy:
# Entry point: the only published ports (HTTP_PORT/HTTPS_PORT, default 80/443), TLS for DOMAIN, routing, security headers.
image: ghcr.io/lesisty7/quotenode/quotenode-frontend@sha256:40a1cdc4fca5cebd9f43d34daef4ba059a88000ff08af821e199566f07260606
# The released frontend image carries the edge config (frontend/caddy/Caddyfile.edge) and the Cloudflare
# ranges, so the edge needs no host files. Same image digest as the frontend service.
command: ["caddy", "run", "--config", "/etc/caddy/Caddyfile.edge", "--adapter", "caddyfile"]
depends_on:
backend:
condition: service_healthy
frontend:
condition: service_healthy
environment:
# Site address for frontend/caddy/Caddyfile.edge: a public host name enables automatic HTTPS; ":80" or
# "http://host" serves plain HTTP on a trusted LAN. Required: an empty site address is not a valid config.
DOMAIN: ${DOMAIN:?set DOMAIN to the public host name, or :80 for plain HTTP on a LAN}
# Optional: address(es) of an operator reverse proxy in front of this edge (default: trust none).
EDGE_TRUSTED_PROXIES: ${EDGE_TRUSTED_PROXIES:-}
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
- "${HTTPS_PORT:-443}:443/udp" # HTTP/3 (QUIC)
volumes:
- caddy_data:/data
- caddy_config:/config
# caddy is the single ingress (TLS, HTTP/3/QUIC, proxying all traffic); give it
# cores for handshake/encryption bursts under load. CPU idle cost ~0.
cpus: ${CADDY_CPUS:-0}
# 96m fits the 4 GB / light-traffic profile. Under heavy TLS load (many concurrent
# handshakes, large QUIC buffers) raise CADDY_MEM_LIMIT — this ingress is untested at scale.
mem_limit: ${CADDY_MEM_LIMIT:-96m}
restart: unless-stopped
logging: *json-file-logging
# Persistent data: database, uploads, generated PDFs, local backup archives, GeoIP database and the entry
# point's certificates/config. Docker prefixes each name with the Compose project name (by default the
# directory name), so keep the project name across upgrades.
volumes:
postgres_data:
backend_uploads:
backend_pdfs:
backend_backups:
backend_geoip:
caddy_data:
caddy_config:
Para reverter, repõe a cópia dos volumes juntamente com a definição da stack guardada. Nunca inicies a definição anterior numa base de dados que a nova versão já migrou.
Coolify
Substitui a definição Compose do recurso pelo docker-compose.coolify.yml do novo pacote (Edit Compose File) e implementa. O Coolify obtém as novas imagens antes de parar os contentores em execução. Consulta Instalação com o Coolify.
Instalações anteriores aos pacotes de versão
Se instalaste o QuoteNode com estes guias antes de existirem pacotes de versão (um docker-compose.yml escrito à mão com imagens :latest), atualiza no mesmo diretório com os mesmos comandos: o upgrade.sh faz a cópia com as ferramentas da nova versão sobre o projeto Compose e os volumes da tua instalação, pelo que antes não é preciso nem um diretório operator/ nem um serviço de cópias. Antes do prepare, acrescenta estas definições ao .env; a instalação em execução ainda não as usa:
DOMAINconfigura agora o novo ponto de entrada (caddy): um nome de anfitrião público obtém HTTPS automaticamente; para HTTP simples na LAN ou num portátil usaDOMAIN=http://<endereço>(por exemplohttp://192.168.1.20ouhttp://localhost).- Se as portas 80/443 estiverem ocupadas, define
HTTP_PORT/HTTPS_PORTem vez de editar o ficheiro Compose.
O pacote mantém os nomes de volumes dos guias anteriores (postgres_data, backend_uploads, backend_pdfs), por isso os teus dados são reutilizados. Executa upgrade.sh a partir do mesmo diretório (ou mantém o mesmo nome de stack no Portainer): com outro nome de projeto, arrancaria com volumes vazios.
Se uma migração parar a atualização
Uma migração pode recusar-se a correr quando encontra dados que não pode alterar sozinha. O backend não arranca e essa migração não altera nada — é totalmente revertida e não deixa nada para reparar no histórico de migrações. Migrações anteriores da mesma atualização podem já ter sido aplicadas, por isso a base de dados fica imediatamente antes do passo recusado e não na versão de partida. Não há pressa nem perda de dados.
Lê o motivo no registo:
docker compose logs backend | grep -i "cannot enforce"
Para as atribuições de secções de propostas, a imagem inclui uma reparação guiada que funciona sem backend ativo — mostra as propostas afetadas e deixa-te escolher o que lhes acontece. Consulta Resolução de problemas de instalação.
Não uses flyway repair nem baseline para ultrapassar uma migração recusada. Não reparam os dados, e uma migração bloqueada não deixa nada para reparar.
Como migrações anteriores podem já estar aplicadas, um arranque falhado por si só nunca permite voltar às imagens anteriores — vê Reverter.
Verificação após a atualização
- Abre
/health— a resposta éUP. - Abre
/actuator/info: o seucommitShaé igual aocommitShado novorelease.json. - Inicia sessão e verifica a nova versão em Admin > Licença.
- Cria uma proposta de teste e gera um PDF.
- Abre uma ligação pública para verificar o encaminhamento.
- Se o SMTP estiver configurado, envia um e-mail de notificação de teste.
Reverter
Nunca executes docker compose down -v — apaga todos os dados, incluindo a base de dados.
O upgrade.sh rollback volta à definição guardada pelo prepare apenas quando é seguro: o ponto de controlo tem de pertencer a esta instalação, o esquema da base de dados tem de ser exatamente o registado — um arranque falhado não o prova, migrações anteriores podem já estar confirmadas — e cada imagem anterior tem de continuar a ser a que estava em execução. Caso contrário, recusa e não altera nada:
../next/operator/upgrade.sh rollback
- Permitido: os
docker-compose.yml,.enverelease.jsonguardados regressam e as imagens anteriores arrancam sem transferência; os serviços acrescentados pela nova versão são removidos, os volumes mantêm-se. - Recusado: não inicies as imagens anteriores à mão. Se uma migração recusou, corrige na nova versão (vê Se uma migração parar a atualização) ou repõe a cópia anterior à atualização — base de dados e ficheiros como uma unidade — como descrito em Cópias de segurança e recuperação. A reposição verifica o arquivo e a sua compatibilidade antes de alterar o que quer que seja e mantém uma cópia de segurança até verificar o resultado.
- As instalações anteriores aos pacotes de versão revertem da mesma forma. Após uma recusa, repõe a cópia de
../quotenode-backupscom ooperator/restore.shdesta versão; a versão atualiza os dados repostos quando arranca.
Formato de versão
O QuoteNode usa versionamento semântico com um sufixo de canal:
v0.9.0-alpha— desenvolvimento inicialv0.9.0-beta— beta públicav0.9.0-rc— versão candidatav0.9.0— versão estável
Uma etiqueta de versão nunca é reutilizada para outras imagens. O release.json e https://quotenode.dev/releases/latest.json indicam a versão, o canal, a etiqueta, o commit e os digests das imagens da versão.