Installation
Actualización de QuoteNode
Cómo comprobar nuevas versiones, actualizar al paquete de una nueva versión y revertir con seguridad.
Actualizar QuoteNode
QuoteNode comprueba automáticamente si hay versiones nuevas. Cuando hay una más reciente, el panel de administración muestra una notificación con la etiqueta de la versión y un enlace a las notas de la versión.
Cada versión se publica como paquete de despliegue: docker-compose.yml (y docker-compose.coolify.yml) con las imágenes de la versión fijadas por digest, env.example, release.json y las herramientas de operator/. Actualizar consiste en sustituir el archivo Compose por el del paquete siguiente. La versión actual es v0.9.8-alpha (paquete).
Cómo actualizar depende de cómo se instaló QuoteNode. La lista y los comandos siguientes son para un directorio de instalación (servidor, LAN, portátil o una carpeta en un NAS con docker-compose.yml y .env). Un stack de Portainer no tiene ese directorio: ve a Stack de Portainer. Coolify: ve a Coolify.
Problema conocido en v0.9.5-alpha: destino de copia de ejemplo
El env.example de v0.9.5-alpha contiene una línea de ejemplo activa, BACKUP_RCLONE_REMOTE=s3:mybucket/backups. Ese destino no está configurado: si la línea llega a tus ajustes, todas las copias de seguridad fallan y una subida fallida no deja ninguna copia local de esa copia. Los ajustes de estas guías nunca la contienen; revisa tu instalación si creaste .env a partir de env.example o le añadiste ajustes de ese archivo durante una actualización.
En un directorio de instalación, comenta exactamente esa línea de ejemplo (un destino real no cambia) y vuelve a crear los contenedores:
# 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
)
En Portainer, elimina la variable del stack BACKUP_RCLONE_REMOTE si tiene ese valor de ejemplo y actualiza el stack. La definición de Coolify de v0.9.5-alpha no pasa esta variable a la aplicación; elimina el valor de ejemplo del recurso antes de actualizar a una versión posterior.
Las copias quedan entonces en el volumen de copias de seguridad. Inicia una copia en el panel de administración (Admin → Security → Backup), comprueba que termina correctamente y organiza una copia fuera del servidor: un destino rclone configurado o copias periódicas de los archivos en otra máquina (Copias de seguridad y recuperación).
Pasar a PostgreSQL 18
Los archivos Compose ya usan postgres:18-alpine, así que las instalaciones nuevas no necesitan nada. Si una versión alfa anterior funcionaba con PostgreSQL 16, migra los datos una vez — un servidor PostgreSQL 18 no arranca sobre un directorio de datos antiguo, y la aplicación nunca lo reescribe automáticamente. Sigue el procedimiento dedicado y seguro: Migrar los datos a PostgreSQL 18 (por línea de comandos o solo con interfaz, según la plataforma).
Cómo funciona la comprobación de versiones
La interfaz de administración descarga https://quotenode.dev/releases/latest.json al iniciar sesión un administrador. Es una petición GET de solo lectura y sin autenticación; no se envía ningún dato de tu instancia. Si el manifiesto no está accesible, la comprobación falla en silencio y el panel funciona con normalidad.
La comprobación y la URL del manifiesto forman parte de la interfaz web ya construida. Definir VITE_RELEASE_CHECK_ENABLED o VITE_RELEASE_MANIFEST_URL en .env o en Coolify no tiene efecto en una versión; esos valores solo se aplican si construyes tú mismo la imagen del frontend desde el código fuente. Para que una instalación no contacte con quotenode.dev, bloquea esa dirección en tu red.
Lista de comprobación previa
Para un directorio de instalación; Portainer y Coolify tienen sus propias secciones más abajo.
- Anota la versión actual:
release.jsonen el directorio de instalación, o Admin > Licencia. - Copia
.envfuera del servidor — contiene secretos que no se pueden regenerar (sobre todoDB_ENCRYPTION_KEY). - Descarga el nuevo paquete junto a la instalación. Esto no cambia nada de lo que está en marcha:
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 -
# ¿Ajustes añadidos o eliminados por la nueva versión? Compara tu .env con la nueva plantilla.
diff <(grep -o '^[A-Z_]*=' .env | sort) <(grep -o '^[A-Z_]*=' ../next/env.example | sort)
- Añade a
.envlos ajustes que necesita la nueva versión (la comparación anterior los enumera; instalaciones anteriores a los paquetes de versión: consulta Instalaciones anteriores a los paquetes de versión). La instalación en marcha aún no los usa. - Crea el punto de control de la actualización y una copia de seguridad verificada con la herramienta del nuevo paquete. No cambia nada de lo que está en marcha y se detiene al primer error:
BACKUP_ARCHIVE_ENCRYPTION_MODE=AGE_RECIPIENT BACKUP_AGE_RECIPIENT=age1... \
../next/operator/upgrade.sh prepare --export-to ../quotenode-backups
El punto de control (en .quotenode-upgrade/, usado solo por esta instalación) registra los docker-compose.yml, .env y release.json instalados, la identidad de las imágenes en ejecución y el estado del esquema de la base de datos. La copia se hace con las herramientas de la nueva versión sobre el proyecto Compose y los volúmenes de esta instalación, queda local en esta ejecución aunque haya copias remotas configuradas y se conserva en ../quotenode-backups solo cuando su SHA-256 coincide. Copia ese directorio fuera del servidor antes de continuar. El destinatario age está en tu Recovery Kit (Copias de seguridad y recuperación).
- Si las copias programadas están activadas, comprueba también que la última se completó (panel de administración).
Procedimiento de actualización
Docker Compose (servidor, LAN, portátil, carpeta en NAS)
Tras la lista de comprobación, ejecuta en el directorio de instalación, para que el proyecto Compose y sus volúmenes sigan siendo los mismos:
../next/operator/upgrade.sh apply
upgrade.sh apply descarga primero todas las imágenes de la nueva versión y se detiene al primer error: si una descarga falla, la instalación sigue funcionando sin cambios. Solo entonces instala docker-compose.yml, release.json, env.example y operator/ del paquete, inicia la nueva versión y espera a que esté sana. Las migraciones de la base de datos se ejecutan automáticamente al arrancar el nuevo backend. Se requiere Docker Compose 2.17 o posterior.
Stack de Portainer
upgrade.sh necesita un directorio de instalación, así que un stack de Portainer se actualiza en Portainer:
- Comprueba que la última copia programada se completó correctamente (panel de administración), protege los volúmenes del stack con la herramienta de instantáneas o copias de tu plataforma y guarda una copia de la definición actual del stack y de sus variables de entorno.
- Abre el Editor del stack y sustituye la definición por el bloque siguiente: la adaptación para Portainer del
docker-compose.ymlde la versión, el mismo bloque que en Instalar con Portainer. No pegues el archivo original del paquete: lee.env, que Portainer no tiene. - Conserva el nombre del stack y sus variables de entorno (
stack.env); añade los ajustes que necesite la nueva versión (compara con suenv.example). - Haz clic en Update the stack con la descarga de imágenes activada. Las migraciones de la base de datos se ejecutan al arrancar el nuevo 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 volver atrás, restaura la copia de los volúmenes junto con la definición del stack guardada. Nunca arranques la definición anterior sobre una base de datos que la nueva versión ya ha migrado.
Coolify
Sustituye la definición Compose del recurso por el docker-compose.coolify.yml del nuevo paquete (Edit Compose File) y despliega. Coolify descarga las imágenes nuevas antes de detener los contenedores en marcha. Consulta Instalación con Coolify.
Instalaciones anteriores a los paquetes de versión
Si instalaste QuoteNode con estas guías antes de que existieran los paquetes de versión (un docker-compose.yml escrito a mano con imágenes :latest), actualiza en el mismo directorio con los mismos comandos: upgrade.sh hace la copia con las herramientas de la nueva versión sobre el proyecto Compose y los volúmenes de tu instalación, así que antes no se necesita ni un directorio operator/ ni un servicio de copias. Antes de prepare, añade estos ajustes a .env; la instalación en marcha aún no los usa:
DOMAINconfigura ahora el nuevo punto de entrada (caddy): un nombre de host público obtiene HTTPS automáticamente; para HTTP simple en LAN o en un portátil usaDOMAIN=http://<dirección>(por ejemplohttp://192.168.1.20ohttp://localhost).- Si los puertos 80/443 están ocupados, define
HTTP_PORT/HTTPS_PORTen lugar de editar el archivo Compose.
El paquete conserva los nombres de volumen de las guías anteriores (postgres_data, backend_uploads, backend_pdfs), así que tus datos se reutilizan. Ejecuta upgrade.sh desde el mismo directorio (o conserva el mismo nombre de stack en Portainer): con otro nombre de proyecto arrancaría con volúmenes vacíos.
Si una migración detiene la actualización
Una migración puede negarse a ejecutarse cuando encuentra datos que no puede modificar por su cuenta. El backend no arranca y esa migración no cambia nada — se revierte por completo y no deja nada que reparar en el historial de migraciones. Migraciones anteriores de la misma actualización pueden haberse aplicado ya, así que tu base de datos queda justo antes del paso rechazado y no en la versión de partida. No hay prisa ni pérdida de datos.
Consulta el motivo en el registro:
docker compose logs backend | grep -i "cannot enforce"
Para las asignaciones de secciones de ofertas, la imagen incluye una reparación guiada que funciona sin backend en marcha — muestra las ofertas afectadas y te deja elegir qué ocurre con ellas. Consulta Solución de problemas de instalación.
No uses flyway repair ni baseline para saltarte una migración rechazada. No reparan los datos, y una migración bloqueada no deja nada que reparar.
Como las migraciones anteriores pueden estar ya aplicadas, un arranque fallido por sí solo nunca permite volver a las imágenes anteriores — consulta Revertir.
Comprobación tras la actualización
- Abre
/health— la respuesta esUP. - Abre
/actuator/info: sucommitShacoincide con elcommitShadel nuevorelease.json. - Inicia sesión y comprueba la nueva versión en Admin > Licencia.
- Crea una oferta de prueba y genera un PDF.
- Abre un enlace público para comprobar el enrutamiento.
- Si SMTP está configurado, envía un correo de notificación de prueba.
Revertir
Nunca ejecutes docker compose down -v — elimina todos los datos, incluida la base de datos.
upgrade.sh rollback vuelve a la definición guardada por prepare solo cuando es seguro: el punto de control debe pertenecer a esta instalación, el esquema de la base de datos debe ser exactamente el registrado — un arranque fallido no lo demuestra, las migraciones anteriores pueden estar ya confirmadas — y cada imagen anterior debe seguir siendo la que funcionaba. En caso contrario, se niega y no cambia nada:
../next/operator/upgrade.sh rollback
- Permitido: los
docker-compose.yml,.envyrelease.jsonguardados vuelven y las imágenes anteriores arrancan sin descargar; los servicios añadidos por la nueva versión se eliminan, los volúmenes se conservan. - Denegado: no inicies las imágenes anteriores a mano. Si una migración se negó, corrígelo en la nueva versión (consulta Si una migración detiene la actualización) o restaura la copia previa a la actualización — base de datos y archivos como una unidad — como se describe en Copias de seguridad y recuperación. La restauración comprueba el archivo y su compatibilidad antes de cambiar nada y conserva una copia de seguridad hasta verificar el resultado.
- Las instalaciones anteriores a los paquetes de versión vuelven atrás de la misma forma. Tras una negativa, restaura la copia de
../quotenode-backupsconoperator/restore.shde esta versión; la versión actualiza los datos restaurados al arrancar.
Formato de versión
QuoteNode usa versionado semántico con un sufijo de canal:
v0.9.0-alpha— desarrollo tempranov0.9.0-beta— beta públicav0.9.0-rc— candidata a versiónv0.9.0— versión estable
Una etiqueta de versión nunca se reutiliza para otras imágenes. release.json y https://quotenode.dev/releases/latest.json indican la versión, el canal, la etiqueta, el commit y los digests de las imágenes de la versión.