Vai al contenuto
Q
QuoteNode

Installation

Installare con Coolify

Distribuire una release di QuoteNode in Coolify dal suo pacchetto di distribuzione: immagini già pronte fissate tramite digest, nulla viene compilato sul tuo server.

Installazione con Coolify

Coolify offre HTTPS gestito, controlli di stato e un’interfaccia web per le variabili d’ambiente. QuoteNode funziona in Coolify a partire da un pacchetto di release: una definizione Compose che scarica le immagini testate della release tramite digest. Sul tuo server non viene compilato nulla, e ogni distribuzione dello stesso pacchetto esegue esattamente le stesse immagini.

Release attuale: v0.9.8-alpha — immagini per server x86 a 64 bit (linux/amd64).

Passo 1 — Crea la risorsa

  1. In Coolify scegli New Resource → Docker Compose Empty.
  2. Incolla senza modifiche la definizione Compose della release per Coolify. È disponibile anche come file docker-compose.coolify.yml:
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 — Coolify deployment (Docker Compose Empty resource)
# 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
#
# Paste into a Coolify "Docker Compose Empty" resource; set the domain on the frontend service and the
# variables in the Coolify UI (runtime only, never build arguments). Coolify routes the domain to frontend.
# env.example of this package describes every setting; enter only the values you need.
# Upgrades: replace the compose definition with the next release package version and redeploy.
# ==========================================================================

services:
  postgres:
    # Database (PostgreSQL 18); internal network only, data in the database volume.
    image: postgres:18-alpine@sha256:77f585114c32fbca283dc835b0596f4e52b51b4c6662d7810b2f4084f60a1873
    # JIT off by default (small OLTP workload); set PG_JIT=on to re-enable.
    command: ["postgres", "-c", "jit=${PG_JIT:-off}"]
    environment:
      POSTGRES_DB: ${DB_NAME:-quotenode}
      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_prod_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d ${DB_NAME:-quotenode}"]
      interval: 10s
      timeout: 5s
      retries: 10
    restart: unless-stopped
    logging: *json-file-logging

  gotenberg:
    # PDF rendering (Chromium) for the backend; internal network only, keeps no data.
    image: gotenberg/gotenberg:8@sha256:aa552cc115db488e0f0bcb07446aac13071ad51505b642c84bb894500907b42b
    environment:
      # Gotenberg requires lowercase log levels (error/warn/info/debug).
      # Explicit override prevents Coolify-injected LOG_LEVEL (uppercase) from crashing it.
      LOG_LEVEL: info
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:3000/health"]
      interval: 10s
      timeout: 5s
      retries: 5
    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
    environment:
      SPRING_PROFILES_ACTIVE: prod
      JOBS_MODE: web
      BACKUP_LOCAL_DIR: /app/data/backups
      DB_URL: ${DB_URL:-jdbc:postgresql://postgres:5432/quotenode}
      DB_USERNAME: ${DB_USERNAME}
      DB_PASSWORD: ${DB_PASSWORD}
      DB_NAME: ${DB_NAME:-quotenode}
      DB_ENCRYPTION_KEY: ${DB_ENCRYPTION_KEY}
      TIMING_TOKEN_SECRET: ${TIMING_TOKEN_SECRET}
      PUBLIC_LINK_PASSWORD_SESSION_SECRET: ${PUBLIC_LINK_PASSWORD_SESSION_SECRET}
      CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
      SMTP_HOST: ${SMTP_HOST:-}
      SMTP_PORT: ${SMTP_PORT:-587}
      SMTP_USERNAME: ${SMTP_USERNAME:-}
      SMTP_PASSWORD: ${SMTP_PASSWORD:-}
      SMTP_AUTH: ${SMTP_AUTH:-true}
      SMTP_STARTTLS: ${SMTP_STARTTLS:-true}
      NOTIFICATIONS_ENABLED: ${NOTIFICATIONS_ENABLED:-true}
      NOTIFICATIONS_EMAIL_ENABLED: ${NOTIFICATIONS_EMAIL_ENABLED:-false}
      NOTIFICATIONS_PUBLIC_PREFERENCES_URL: ${NOTIFICATIONS_PUBLIC_PREFERENCES_URL:-}
      # Absolute public app base URL for links in notification emails. Blank ⇒ the origin of
      # NOTIFICATIONS_PUBLIC_PREFERENCES_URL is used; with email enabled a valid http/https base must
      # resolve or the backend fails fast so no email carries a dead relative link.
      NOTIFICATIONS_APP_BASE_URL: ${NOTIFICATIONS_APP_BASE_URL:-}
      # Offer-revisit notification scheduler kill-switch + interval. Must be declared here (not only in
      # the panel) to reach the container. Default on; stops only the periodic revisit sweep (in-app
      # notifications from other paths are unaffected). Interval must be > 0 (fail-fast at startup).
      NOTIFICATIONS_REVISIT_SCHEDULER_ENABLED: ${NOTIFICATIONS_REVISIT_SCHEDULER_ENABLED:-true}
      NOTIFICATIONS_REVISIT_CHECK_INTERVAL_SECONDS: ${NOTIFICATIONS_REVISIT_CHECK_INTERVAL_SECONDS:-7200}
      # Calendar-reminder scheduler kill-switch. Must be declared here (not only in the panel) to reach the
      # container. Default on; stops only the periodic reminder sweep (in-app notifications from other paths
      # are unaffected). The cadence is the validated calendar.reminders-job-interval-minutes (grace >=
      # interval enforced at startup); in-app only unless NOTIFICATIONS_EMAIL_ENABLED=true.
      CALENDAR_REMINDERS_SCHEDULER_ENABLED: ${CALENDAR_REMINDERS_SCHEDULER_ENABLED:-true}
      # Reminder-scheduler cadences (minutes). Two independent knobs — the calendar and CRM reminder
      # schedulers never share one; each falls back to the legacy REMINDERS_JOB_INTERVAL_MINUTES, then 5.
      # Must be declared here (not only in the panel) to reach the container.
      CALENDAR_REMINDERS_JOB_INTERVAL_MINUTES: ${CALENDAR_REMINDERS_JOB_INTERVAL_MINUTES:-${REMINDERS_JOB_INTERVAL_MINUTES:-5}}
      NOTIFICATIONS_REMINDER_CHECK_INTERVAL_MINUTES: ${NOTIFICATIONS_REMINDER_CHECK_INTERVAL_MINUTES:-${REMINDERS_JOB_INTERVAL_MINUTES:-5}}
      # CRM-reminder delivery scheduler kill-switch (Wave 4D). Must be declared here (not only in the panel)
      # to reach the container. Default on. Set false for the FIRST redeploy of an existing install so the
      # newly-activated scheduler cannot notify a legacy backlog; the V121 migration also fail-closes overdue
      # rows to FAILED, so a normal deploy will not emit historical reminders.
      NOTIFICATIONS_REMINDER_CHECK_SCHEDULER_ENABLED: ${NOTIFICATIONS_REMINDER_CHECK_SCHEDULER_ENABLED:-true}
      # Customer-asset reminder scheduler kill-switch (warranty/subscription/maintenance). Must be declared
      # here (not only in the panel) to reach the container. Default on. The reminder window's upper bound
      # keeps long-past deadlines from flooding on activation, so a normal deploy will not emit a backlog.
      ASSETS_NOTIFICATION_SCHEDULER_ENABLED: ${ASSETS_NOTIFICATION_SCHEDULER_ENABLED:-true}
      # Dedicated session-retention purge scheduler kill-switch. Default on: it only removes already-expired
      # sessions older than the retention window. Web backend only — never the backup-worker.
      SESSION_RETENTION_SCHEDULER_ENABLED: ${SESSION_RETENTION_SCHEDULER_ENABLED:-true}
      # Dedicated database-health scheduler kill-switch. Default on: the checks are read-only. Web backend only.
      DATABASE_HEALTH_SCHEDULER_ENABLED: ${DATABASE_HEALTH_SCHEDULER_ENABLED:-true}
      # Dedicated license-expiry reminder scheduler kill-switch. Must be declared here (not only in the panel) to
      # reach the container. Default on: at-most-once (V126 ledger), fires only on an exact warn-day, replays no
      # backlog. Web backend only — never the backup-worker.
      LICENSE_EXPIRY_SCHEDULER_ENABLED: ${LICENSE_EXPIRY_SCHEDULER_ENABLED:-true}
      # Dedicated license-reload scheduler kill-switch. Must be declared here (not only in the panel) to reach the
      # container. Default on: converges a running instance to the authoritative license record without a
      # restart; a read failure keeps last-known-good. Web backend only — never the backup-worker.
      LICENSE_RELOAD_SCHEDULER_ENABLED: ${LICENSE_RELOAD_SCHEDULER_ENABLED:-true}
      # Dedicated offer-expiry reminder scheduler kill-switch. Must be declared here (not only in the panel) to
      # reach the container. Default on: at-most-once (V127 ledger), fires only on an exact warn-day, bounded
      # batch sweep, no status mutation. Web backend only — never the backup-worker.
      OFFER_EXPIRY_SCHEDULER_ENABLED: ${OFFER_EXPIRY_SCHEDULER_ENABLED:-true}
      # Dedicated auto-expire scheduler kill-switch. Default OFF: the first run MUTATES existing data (transitions
      # past-due offers to EXPIRED). An operator enables it after inspecting the backlog. Web backend only.
      OFFERS_AUTO_EXPIRE_SCHEDULER_ENABLED: ${OFFERS_AUTO_EXPIRE_SCHEDULER_ENABLED:-false}
      # Customer-facing operational asset-email feature master switch (a separate, consent-gated channel to the
      # customer's own address). Must be declared here (not only in the panel) to reach the container. Default
      # OFF: nothing is enqueued and the delivery worker stays inert until an operator turns it on and a
      # customer has granted consent.
      ASSETS_CUSTOMER_EMAIL_ENABLED: ${ASSETS_CUSTOMER_EMAIL_ENABLED:-false}
      # Daily email-digest scheduler kill-switch. Must be declared here (not only in the panel) to reach the
      # container. Default on; active only when NOTIFICATIONS_EMAIL_ENABLED=true. Set false for the FIRST
      # redeploy of an existing install with SMTP on, inspect the backlog query, then enable — so a hardened
      # sweep never drains an unknown legacy PENDING backlog on activation.
      NOTIFICATIONS_DIGEST_SCHEDULER_ENABLED: ${NOTIFICATIONS_DIGEST_SCHEDULER_ENABLED:-true}
      # Digest cadence (Spring 6-field cron), operator-tunable. Default hourly on the top of the hour.
      NOTIFICATIONS_DIGEST_CRON: ${NOTIFICATIONS_DIGEST_CRON:-0 0 * * * *}
      # Max events reserved into a single digest (caps message size); the overflow rolls to an auto-scheduled
      # continuation at the user's next digest time. Operator-tunable.
      NOTIFICATIONS_DIGEST_MAX_EVENTS_PER_DIGEST: ${NOTIFICATIONS_DIGEST_MAX_EVENTS_PER_DIGEST:-100}
      # Immediate-notification email delivery worker kill-switch. Must be declared here (not only in the
      # panel) to reach the container. Default true; active only when NOTIFICATIONS_EMAIL_ENABLED=true.
      # Set false for the FIRST redeploy of an existing install so the worker cannot drain an unknown
      # stale QUEUED backlog on activation; the max-queue-age preflight also fails stale rows without SMTP.
      NOTIFICATIONS_EMAIL_WORKER_ENABLED: ${NOTIFICATIONS_EMAIL_WORKER_ENABLED:-true}
      NOTIFICATIONS_TRANSACTIONAL_EMAIL_WORKER_ENABLED: ${NOTIFICATIONS_TRANSACTIONAL_EMAIL_WORKER_ENABLED:-true}
      # Offer-email delivery worker kill-switch. Must be listed here, not only set in the Coolify
      # panel: a panel value is used for interpolation but never reaches the container unless the key
      # is declared in environment. Default true (new, unattended installs). Set false for the FIRST
      # redeploy of an existing install so the worker cannot drain an unknown stale backlog on
      # activation; audit the backlog, then set true and redeploy only this service.
      OFFERS_DELIVERY_WORKER_ENABLED: ${OFFERS_DELIVERY_WORKER_ENABLED:-true}
      # IP-whitelist lockout-safety confirmation/rollback tuning. All three keys must be declared here
      # (not only in the panel) to reach the container. The periodic deadline rollback is a web-tier
      # concern only (never the backup-only worker below), and turning it off leaves the startup
      # fail-safe intact. The check interval (seconds) must be <= the confirmation window, or startup
      # fails fast (IpWhitelistConfirmationConfig). Confirmation window = how long an enable/narrow
      # stays pending before it is rolled back if unconfirmed (default 120s).
      SECURITY_IP_WHITELIST_CONFIRMATION_WINDOW_SECONDS: ${SECURITY_IP_WHITELIST_CONFIRMATION_WINDOW_SECONDS:-120}
      SECURITY_IP_WHITELIST_ROLLBACK_SCHEDULER_ENABLED: ${SECURITY_IP_WHITELIST_ROLLBACK_SCHEDULER_ENABLED:-true}
      SECURITY_IP_WHITELIST_ROLLBACK_CHECK_INTERVAL_SECONDS: ${SECURITY_IP_WHITELIST_ROLLBACK_CHECK_INTERVAL_SECONDS:-15}
      PDF_ENABLED: ${PDF_ENABLED:-true}
      # Async PDF worker kill-switch. Must be declared here (not only in the panel) to reach the
      # container. Default true; set false only to inspect the queue during a controlled rollout or a
      # Gotenberg/worker incident — sync generation and downloads keep working while it is off.
      PDF_WORKER_ENABLED: ${PDF_WORKER_ENABLED:-true}
      # Worker tuning must be declared here to reach the container (a panel value alone is only used for
      # interpolation). Defaults match application.yml; override for a controlled backlog drain.
      PDF_WORKER_CONCURRENCY: ${PDF_WORKER_CONCURRENCY:-2}
      PDF_WORKER_POLL_INTERVAL_MS: ${PDF_WORKER_POLL_INTERVAL_MS:-2000}
      PDF_WORKER_RECOVERY_INTERVAL_MS: ${PDF_WORKER_RECOVERY_INTERVAL_MS:-60000}
      PDF_WORKER_CLAIM_TIMEOUT_SECONDS: ${PDF_WORKER_CLAIM_TIMEOUT_SECONDS:-300}
      PDF_RETRY_BACKOFF_SECONDS: ${PDF_RETRY_BACKOFF_SECONDS:-30,120,600}
      PDF_GOTENBERG_URL: http://gotenberg:3000
      LICENSE_SKIP_SIGNATURE_CHECK: ${LICENSE_SKIP_SIGNATURE_CHECK:-false}
      LOG_LEVEL: ${LOG_LEVEL:-ERROR}
      BACKUP_ENABLED: ${BACKUP_ENABLED:-false}
      BACKUP_ARCHIVE_ENCRYPTION_MODE: ${BACKUP_ARCHIVE_ENCRYPTION_MODE:-}
      BACKUP_AGE_RECIPIENT: ${BACKUP_AGE_RECIPIENT:-}
      BACKUP_AGE_BINARY_PATH: ${BACKUP_AGE_BINARY_PATH:-age}
      BACKUP_AGE_KEYGEN_BINARY_PATH: ${BACKUP_AGE_KEYGEN_BINARY_PATH:-age-keygen}
      BACKUP_VERIFIER_SCRIPT_PATH: ${BACKUP_VERIFIER_SCRIPT_PATH:-/app/scripts/quotenode-backup-verify.sh}
      BACKUP_POWERSHELL_VERIFIER_SCRIPT_PATH: ${BACKUP_POWERSHELL_VERIFIER_SCRIPT_PATH:-/app/scripts/quotenode-backup-verify.ps1}
      BACKUP_ALLOW_UNENCRYPTED_LOCAL_BACKUP: ${BACKUP_ALLOW_UNENCRYPTED_LOCAL_BACKUP:-false}
      BACKUP_RUNTIME_PROFILE: ${BACKUP_RUNTIME_PROFILE:-WORKER}
      BACKUP_DATABASE_PAYLOAD_MODE: ${BACKUP_DATABASE_PAYLOAD_MODE:-APP_ENCRYPTED}
      BACKUP_VERIFY_MODE: ${BACKUP_VERIFY_MODE:-off}
      BACKUP_DOWNLOAD_GRANT_TTL_SECONDS: ${BACKUP_DOWNLOAD_GRANT_TTL_SECONDS:-120}
      # Off-host copy (rclone remote, e.g. offsite:quotenode); empty = archives stay in the backups volume.
      # The remote's credentials are rclone settings outside this contract (see the Coolify guide).
      BACKUP_RCLONE_REMOTE: ${BACKUP_RCLONE_REMOTE:-}
      BACKUP_RETENTION_DAILY: ${BACKUP_RETENTION_DAILY:-7}
      BACKUP_RETENTION_CLEANUP_BATCH_SIZE: ${BACKUP_RETENTION_CLEANUP_BATCH_SIZE:-20}
      BACKUP_RETENTION_MAX_SCAN: ${BACKUP_RETENTION_MAX_SCAN:-5000}
      FEATURE_2FA_ENABLED: ${FEATURE_2FA_ENABLED:-true}
      # Roles that must use two-factor authentication (e.g. ADMIN); empty = the application default (none).
      SECURITY_REQUIRE_2FA_ROLES: ${SECURITY_REQUIRE_2FA_ROLES:-}
      SECURITY_BOT_DETECTION_ENABLED: ${SECURITY_BOT_DETECTION_ENABLED:-true}
      GEOIP_ENABLED: ${GEOIP_ENABLED:-false}
      # GeoIP enforcement needs a country .mmdb at this path. The backend fetches it automatically
      # (GEOIP_AUTO_UPDATE; keyless DB-IP Country Lite by default, or MaxMind GeoLite2 when MAXMIND_* are
      # set); Update in admin/security fetches on demand. Empty/missing DB => GeoIP fails OPEN.
      GEOIP_DB_PATH: ${GEOIP_DB_PATH:-/app/data/geoip/GeoLite2-Country.mmdb}
      # Database lifecycle, independent of enforcement. Declared here so a panel value actually
      # reaches the container: an interpolation-only value never does. auto-update governs every
      # automatic fetch; the scheduler switch governs only the periodic staleness check, leaving the
      # readiness-time refresh and manual updates alone.
      GEOIP_AUTO_UPDATE: ${GEOIP_AUTO_UPDATE:-true}
      GEOIP_UPDATE_SCHEDULER_ENABLED: ${GEOIP_UPDATE_SCHEDULER_ENABLED:-true}
      GEOIP_UPDATE_CRON: "${GEOIP_UPDATE_CRON:-0 15 3 * * *}"
      # GeoIP database source for the bundled updater: empty = auto (keyless DB-IP Country Lite, or MaxMind
      # GeoLite2 when both MAXMIND_* values are set). Web tier only, like the other GeoIP settings.
      GEOIP_PROVIDER: ${GEOIP_PROVIDER:-}
      MAXMIND_ACCOUNT_ID: ${MAXMIND_ACCOUNT_ID:-}
      MAXMIND_LICENSE_KEY: ${MAXMIND_LICENSE_KEY:-}
      GEOIP_EDITION_ID: ${GEOIP_EDITION_ID:-GeoLite2-Country}
      # Client-IP retention. TRACKING_IP_ANONYMIZE plus its delay were documented in the env examples
      # but never declared here, so they only ever fed interpolation and could not reach the container
      # — the privacy policy was unreachable in exactly the way the sweep itself was. The sweep is
      # irreversible, so it ships off and both retention windows ship at zero.
      TRACKING_IP_ANONYMIZE: ${TRACKING_IP_ANONYMIZE:-false}
      TRACKING_IP_ANONYMIZE_AFTER_DAYS: ${TRACKING_IP_ANONYMIZE_AFTER_DAYS:-0}
      TRACKING_IP_ANONYMIZATION_SCHEDULER_ENABLED: ${TRACKING_IP_ANONYMIZATION_SCHEDULER_ENABLED:-false}
      TRACKING_IP_ANONYMIZATION_CRON: "${TRACKING_IP_ANONYMIZATION_CRON:-0 0 3 * * *}"
      TRACKING_IP_ANONYMIZATION_BATCH_SIZE: ${TRACKING_IP_ANONYMIZATION_BATCH_SIZE:-1000}
      TRACKING_IP_ANONYMIZATION_MAX_BATCHES_PER_TABLE: ${TRACKING_IP_ANONYMIZATION_MAX_BATCHES_PER_TABLE:-50}
      SECURITY_AUTH_IP_RETENTION_DAYS: ${SECURITY_AUTH_IP_RETENTION_DAYS:-0}
      # Import cleanup. Declared here so a panel value actually reaches the container: an
      # interpolation-only value never does. The purge is irreversible, so it ships off.
      IMPORT_CLEANUP_SCHEDULER_ENABLED: ${IMPORT_CLEANUP_SCHEDULER_ENABLED:-false}
      IMPORT_CLEANUP_INTERVAL_MS: ${IMPORT_CLEANUP_INTERVAL_MS:-86400000}
      IMPORT_CLEANUP_MAX_SESSIONS_PER_RUN: ${IMPORT_CLEANUP_MAX_SESSIONS_PER_RUN:-500}
      IMPORT_CLEANUP_PURGE_AFTER_DAYS: ${IMPORT_CLEANUP_PURGE_AFTER_DAYS:-7}
      # Client-IP trust + IP-whitelist controls (audit P1-12; security finding #80). Forwarding headers
      # are honored ONLY from an explicitly trusted proxy peer — a private/loopback/CGNAT/ULA peer is
      # NOT auto-trusted. The frontend Caddy reverse-proxies /api to this backend, so it is the trusted
      # peer: it is declared by its Compose service name below and works with NO manual configuration
      # (resolved to the container IP outside the request path, re-resolved on a schedule). Override
      # only for a custom proxy network (extra CIDR/hostname); a malformed IP/CIDR fails startup.
      # SECURITY_IP_WHITELIST_FORCE_DISABLE=true + restart is the break-glass recovery for a lockout.
      SECURITY_TRUSTED_PROXIES: ${SECURITY_TRUSTED_PROXIES:-frontend}
      SECURITY_IP_WHITELIST: ${SECURITY_IP_WHITELIST:-}
      SECURITY_IP_WHITELIST_FORCE_DISABLE: ${SECURITY_IP_WHITELIST_FORCE_DISABLE:-false}
      SECURITY_GEOIP_ALLOWED_COUNTRIES: ${SECURITY_GEOIP_ALLOWED_COUNTRIES:-}
    volumes:
      - backend_prod_uploads:/app/data/uploads
      - backend_prod_pdfs:/app/data/pdfs
      - backend_prod_backups:/app/data/backups
      - backend_prod_geoip:/app/data/geoip
    # No published port (security finding #80): the frontend Caddy reaches the backend over the
    # internal Compose network (backend:8080), so publishing it would only expose a docker-NAT peer
    # that could be used to reach the app directly. Internal-only keeps the frontend Caddy the sole
    # trusted ingress for client-IP resolution.
    expose:
      - "8080"
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
      interval: 15s
      timeout: 10s
      retries: 5
      start_period: 45s
    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
    environment:
      SPRING_PROFILES_ACTIVE: prod
      JOBS_MODE: backup-only
      BACKUP_LOCAL_DIR: /app/data/backups
      DB_URL: ${DB_URL:-jdbc:postgresql://postgres:5432/quotenode}
      DB_USERNAME: ${DB_USERNAME}
      DB_PASSWORD: ${DB_PASSWORD}
      DB_NAME: ${DB_NAME:-quotenode}
      DB_ENCRYPTION_KEY: ${DB_ENCRYPTION_KEY}
      TIMING_TOKEN_SECRET: ${TIMING_TOKEN_SECRET}
      PUBLIC_LINK_PASSWORD_SESSION_SECRET: ${PUBLIC_LINK_PASSWORD_SESSION_SECRET}
      CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
      BACKUP_ENABLED: ${BACKUP_ENABLED:-false}
      BACKUP_CRON: ${BACKUP_CRON:-0 0 2 * * *}
      # Dedicated backup-scheduler kill-switch. Default OFF and separate from BACKUP_ENABLED. Declared ONLY on the
      # backup-only worker — the web backend (JOBS_MODE=web) must never run the periodic backup.
      BACKUP_SCHEDULER_ENABLED: ${BACKUP_SCHEDULER_ENABLED:-false}
      BACKUP_ARCHIVE_ENCRYPTION_MODE: ${BACKUP_ARCHIVE_ENCRYPTION_MODE:-}
      BACKUP_AGE_RECIPIENT: ${BACKUP_AGE_RECIPIENT:-}
      BACKUP_AGE_BINARY_PATH: ${BACKUP_AGE_BINARY_PATH:-age}
      BACKUP_AGE_KEYGEN_BINARY_PATH: ${BACKUP_AGE_KEYGEN_BINARY_PATH:-age-keygen}
      BACKUP_VERIFIER_SCRIPT_PATH: ${BACKUP_VERIFIER_SCRIPT_PATH:-/app/scripts/quotenode-backup-verify.sh}
      BACKUP_POWERSHELL_VERIFIER_SCRIPT_PATH: ${BACKUP_POWERSHELL_VERIFIER_SCRIPT_PATH:-/app/scripts/quotenode-backup-verify.ps1}
      BACKUP_ALLOW_UNENCRYPTED_LOCAL_BACKUP: ${BACKUP_ALLOW_UNENCRYPTED_LOCAL_BACKUP:-false}
      BACKUP_RUNTIME_PROFILE: ${BACKUP_RUNTIME_PROFILE:-WORKER}
      BACKUP_DATABASE_PAYLOAD_MODE: ${BACKUP_DATABASE_PAYLOAD_MODE:-APP_ENCRYPTED}
      BACKUP_VERIFY_MODE: ${BACKUP_VERIFY_MODE:-off}
      # Off-host copy (rclone remote, e.g. offsite:quotenode); empty = archives stay in the backups volume.
      # The remote's credentials are rclone settings outside this contract (see the Coolify guide).
      BACKUP_RCLONE_REMOTE: ${BACKUP_RCLONE_REMOTE:-}
      BACKUP_RETENTION_DAILY: ${BACKUP_RETENTION_DAILY:-7}
      BACKUP_RETENTION_CLEANUP_BATCH_SIZE: ${BACKUP_RETENTION_CLEANUP_BATCH_SIZE:-20}
      BACKUP_RETENTION_MAX_SCAN: ${BACKUP_RETENTION_MAX_SCAN:-5000}
      LICENSE_SKIP_SIGNATURE_CHECK: ${LICENSE_SKIP_SIGNATURE_CHECK:-false}
      LOG_LEVEL: ${LOG_LEVEL:-ERROR}
    volumes:
      - backend_prod_uploads:/app/data/uploads
      - backend_prod_pdfs:/app/data/pdfs
      - backend_prod_backups:/app/data/backups
      - backend_prod_geoip:/app/data/geoip
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
      interval: 15s
      timeout: 10s
      retries: 5
      start_period: 45s
    restart: unless-stopped
    logging: *json-file-logging

  frontend:
    # Web UI and entry point: Coolify routes the resource domain here; it proxies the API to the backend.
    image: ghcr.io/lesisty7/quotenode/quotenode-frontend@sha256:40a1cdc4fca5cebd9f43d34daef4ba059a88000ff08af821e199566f07260606
    depends_on:
      backend:
        condition: service_healthy
    # No published host port (security finding #80): Coolify's own proxy is the sole ingress and reaches
    # this frontend Caddy over the internal Coolify network. Publishing a host port would expose the
    # Caddy directly via a docker-NAT peer, letting a client spoof the client IP through it. Both backend
    # and frontend stay internal-only; the frontend Caddy uses trusted_proxies_strict so even the
    # legitimate Coolify edge cannot be tricked by a forged left-most X-Forwarded-For.
    expose:
      - "80"
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:80/"]
      interval: 15s
      timeout: 5s
      retries: 3
    restart: unless-stopped
    logging: *json-file-logging

# Persistent data: database, uploads, generated PDFs, local backup archives and the GeoIP database.
# Coolify prefixes each name with the resource UUID, so keep the same resource across upgrades.
volumes:
  postgres_prod_data:
  backend_prod_uploads:
  backend_prod_pdfs:
  backend_prod_backups:
  backend_prod_geoip:
  1. Imposta il dominio pubblico (ad esempio https://crm.example.com) solo sul servizio frontend. Lascia backend, backup-worker, postgres e gotenberg senza dominio: restano interni e il frontend è l’unico punto di ingresso.

Passo 2 — Variabili d’ambiente

Impostale in Environment Variables della risorsa. Sono valori di esecuzione: le immagini di QuoteNode sono già costruite, quindi non va passato nulla come argomento di build.

DB_NAME=quotenode
DB_USERNAME=quotenode
DB_PASSWORD=<random-32+-characters>
DB_URL=jdbc:postgresql://postgres:5432/quotenode
DB_ENCRYPTION_KEY=<64-hex-characters>
TIMING_TOKEN_SECRET=<random-32+-characters>
PUBLIC_LINK_PASSWORD_SESSION_SECRET=<random-32+-characters>
CORS_ALLOWED_ORIGINS=https://crm.example.com
NOTIFICATIONS_PUBLIC_PREFERENCES_URL=https://crm.example.com/notifications/preferences
LOG_LEVEL=INFO

Se l’invio di e-mail è attivo, imposta anche:

NOTIFICATIONS_EMAIL_ENABLED=true
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME[email protected]
SMTP_PASSWORD=<smtp-password>
SMTP_AUTH=true
SMTP_STARTTLS=true

Tutte le altre impostazioni hanno valori predefiniti sicuri nella definizione Compose (ad esempio BACKUP_ENABLED=false, GEOIP_ENABLED=false). Il rilevamento dell’IP del client funziona senza configurazione: il frontend è il proxy attendibile del backend. L’elenco completo è in Variabili d’ambiente della distribuzione.

Solo le impostazioni dichiarate nella definizione Compose raggiungono i container; env.example è il catalogo del pacchetto standalone, in cui ogni impostazione raggiunge l’applicazione. Per usare un’altra impostazione, aggiungi KEY: ${KEY} all’environment: del servizio che la legge (Edit Compose File; impostazioni dell’applicazione: backend, impostazioni di backup anche backup-worker), imposta il valore nel pannello e ripeti l’aggiunta dopo ogni aggiornamento. Così funziona una copia di backup fuori dal server: BACKUP_RCLONE_REMOTE (se la definizione non lo dichiara ancora) e le impostazioni RCLONE_CONFIG_<NAME>_* della destinazione su backend e backup-worker.

Lascia LICENSE_SKIP_SIGNATURE_CHECK non impostato o false: in produzione l’applicazione non si avvia con questa opzione attiva. Le impostazioni che iniziano con VITE_ non hanno effetto su una release: l’interfaccia web è già costruita e il controllo degli aggiornamenti punta a quotenode.dev.

Conserva i segreti al di fuori di Coolify. DB_ENCRYPTION_KEY e gli altri segreti servono per ripristinare un backup su un nuovo server.

Passo 3 — Tempo di arresto

Nelle impostazioni Advanced della risorsa imposta lo stop grace period a 60 secondi. Durante una distribuzione Coolify arresta i vecchi container con un proprio limite di tempo (30 secondi per impostazione predefinita) e ignora il valore del file Compose. 60 secondi permettono al backend di completare l’invio delle e-mail e chiudere correttamente le richieste. Le versioni di Coolify più vecchie senza questa impostazione usano sempre 30 secondi.

Passo 4 — Distribuisci e verifica

Fai clic su Deploy. Coolify scarica le immagini e avvia i servizi; al primo avvio il backend esegue le migrazioni del database.

  1. Apri https://crm.example.com/health — la risposta è UP.
  2. Accedi con [email protected] / Admin123! e imposta una nuova password quando richiesto.
  3. Crea un’offerta, genera il suo PDF e apri il suo link pubblico da un altro dispositivo.
  4. Se le e-mail sono attive, invia una notifica di prova e apri il link alle preferenze che contiene.

Ogni distribuzione ricrea tutti i container della risorsa, compreso il database; i dati restano nei volumi. Considera una breve interruzione a ogni distribuzione.

Aggiornamento

Sostituisci la definizione Compose con il docker-compose.coolify.yml della release successiva (Edit Compose File) e distribuisci. Coolify gestisce il download delle immagini e la sostituzione dei container; i volumi conservano i dati. Verifica nel log che la tua versione di Coolify scarichi tutte le immagini necessarie prima di arrestare i container in esecuzione. Esegui prima un backup e leggi Aggiornare QuoteNode.

Una definizione più recente può dichiarare impostazioni che la precedente ignorava: prima di distribuirla, rimuovi dal pannello i valori che non devono avere effetto (ad esempio l’esempio BACKUP_RCLONE_REMOTE=s3:mybucket/backups di env.example) e riaggiungi le tue righe KEY: ${KEY}.

Problemi comuni con Coolify

  • I link pubblici non funzionano quando CORS_ALLOWED_ORIGINS non corrisponde esattamente all’origine del browser (schema, senza barra finale).
  • I link alle preferenze nelle e-mail non funzionano quando NOTIFICATIONS_PUBLIC_PREFERENCES_URL punta ancora a localhost o a un vecchio dominio.
  • L’HTTPS gestito da Coolify richiede che il dominio punti al server Coolify.
  • Non assegnare un dominio a backend, postgres, gotenberg o backup-worker; esporli aggira l’instradamento del frontend e la protezione dell’IP del client.
  • Una variabile del pannello raggiunge solo i servizi il cui environment: Compose la dichiara; Gotenberg mantiene il proprio livello di log fissato.

Last reviewed: Recently