Przejdź do treści
Q
QuoteNode

Installation

Instalacja na lokalnym laptopie

Uruchom QuoteNode lokalnie w celu ewaluacji, bez udostępniania klientom.

Instalacja na lokalnym laptopie

Wybierz tę ścieżkę, gdy chcesz wypróbować QuoteNode na jednym komputerze. To najszybszy sposób na ewaluację aplikacji, ale nie jest to wdrożenie dostępne dla klientów.

Kiedy użyć

  • Chcesz uruchomić demo lub instancję próbną.
  • Testujesz tworzenie ofert, PDF-y, szablony, produkty i ustawienia.
  • Nie potrzebujesz, aby klienci otwierali publiczne linki do ofert z innego urządzenia.

Co nie zadziała

  • Publiczne linki do ofert generowane jako http://localhost/... działają tylko na tym samym laptopie.
  • Linki w e-mailach nie będą użyteczne, bo odbiorcy nie mogą otworzyć localhost ze swoich maszyn.
  • HTTPS nie jest normalnie konfigurowany w tej topologii.
  • Laptop musi być włączony, a Docker uruchomiony.

Wymagania

  • Docker Desktop lub Docker Engine z Docker Compose v2.
  • Co najmniej 2,5 GB RAM dostępne dla stacku.
  • Co najmniej 10 GB wolnego miejsca na dysku.
  • Host 64-bitowy x86 (linux/amd64). Urządzenia ARM, w tym serwery NAS z procesorem ARM, nie są obsługiwane.

Sprawdź konfigurację:

docker --version        # Should show 24.0+
docker compose version  # Should show v2.x

Krok 1 — Utwórz katalog projektu

mkdir quotenode && cd quotenode
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

Pobierasz pakiet wdrożeniowy wydania v0.9.8-alpha: docker-compose.yml z obrazami przypiętymi po digeście, env.example, release.json (tożsamość wydania) oraz operator/ (narzędzia do kopii zapasowych i odtwarzania).

Znany problem w v0.9.5-alpha: jego env.example zawiera aktywną przykładową linię BACKUP_RCLONE_REMOTE=s3:mybucket/backups, przez którą każda kopia zapasowa kończy się błędem. Ustawienia z tego przewodnika jej nie zawierają; jeśli kopiujesz ustawienia z env.example, pomiń tę linię (szczegóły).

Krok 2 — Utwórz plik konfiguracyjny

Utwórz plik .env z minimalnymi wymaganymi ustawieniami. Wygeneruj własne sekrety — nie używaj wartości zastępczych w produkcji:

# 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 — READ THIS BEFORE CONTINUING
# These values are UNIQUE to your installation.
# If you lose this file, you CANNOT regenerate the same secrets.
# 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

# Domain (for CORS and Caddy)
CORS_ALLOWED_ORIGINS=http://localhost
DOMAIN=http://localhost

# Client IP is auto-detected behind internal proxies and CDNs (Cloudflare included) — leave empty.
# Set only for an unusual proxy whose transport peer is a PUBLIC address to trust (IP/CIDR or hostname).
SECURITY_TRUSTED_PROXIES=

# Application
LOG_LEVEL=INFO
SPRING_PROFILES_ACTIVE=prod

# Smaller laptop profile (see System resources)
POSTGRES_MEM_LIMIT=384m
GOTENBERG_MEM_LIMIT=384m
BACKEND_MEM_LIMIT=1280m
BACKEND_CPUS=2.0

Zapisz plik .env natychmiast. DB_ENCRYPTION_KEY szyfruje wrażliwe dane. Jeśli go stracisz, zaszyfrowane pola (kody MFA, hasło SMTP, klucze API) staną się trwale nieczytelne. Skopiuj ten plik w bezpieczne miejsce.

Krok 3 — Sprawdź plik Docker Compose

Plik docker-compose.yml z pakietu uruchamia wydanie v0.9.8-alpha z obrazami przypiętymi po digeście. Nie edytuj go: wszystkie ustawienia trafiają do .env, a aktualizacja zastępuje ten plik wersją z kolejnego wydania. Dla porządku jego zawartość:

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: .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: .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: .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:

DOMAIN określa, jak punkt wejścia caddy udostępnia QuoteNode: publiczna nazwa hosta (crm.example.com) dostaje automatyczny certyfikat HTTPS; http://<adres> albo :80 udostępnia zwykłe HTTP w zaufanej sieci. Jeśli porty 80/443 są zajęte, ustaw HTTP_PORT / HTTPS_PORT w .env. Za własnym reverse proxy ustaw DOMAIN=:80 i wpisz adres proxy w EDGE_TRUSTED_PROXIES.

Krok 4 — Uruchom QuoteNode

docker compose up -d

Docker pobierze obrazy kontenerów (za pierwszym razem zajmie to kilka minut). Obserwuj postęp:

docker compose logs -f backend

Poczekaj aż zobaczysz Started QuoteNodeApplication — to oznacza, że backend jest gotowy.

Krok 5 — Otwórz QuoteNode

Przejdź do http://localhost w przeglądarce.

Domyślne dane logowania:

Pole Wartość
Email [email protected]
Hasło Admin123!

Zmień domyślne hasło natychmiast po zalogowaniu (Settings > My Profile > Change Password).

Co działa?

Po docker compose up masz 6 kontenery:

Kontener Funkcja Użycie RAM
postgres Przechowuje wszystkie dane ~256 MB
backend Serwer API Java, logika biznesowa ~512 MB – 1,5 GB
gotenberg Konwersja HTML do PDF (oparty na Chromium) ~200 MB
frontend Udostępnia interfejs WWW ~20 MB
backup-worker Wykonuje zaplanowane kopie zapasowe (ten sam obraz co backend) ~300 MB
caddy Punkt wejścia: certyfikaty HTTPS, routing, nagłówki bezpieczeństwa ~20 MB

Łącznie: około 1,5 – 2,5 GB RAM.

Przydatne polecenia

# Check if all services are running
docker compose ps

# View logs (all services)
docker compose logs -f

# View logs (backend only)
docker compose logs -f backend

# Stop QuoteNode
docker compose down

# Aktualizacja: zastąp docker-compose.yml plikiem z kolejnego pakietu wydania (zobacz przewodnik aktualizacji)

Bezpieczeństwo sekretów

Plik .env zawiera sekrety kryptograficzne unikalne dla tej instalacji:

Sekret Co chroni Jeśli utracony
DB_PASSWORD Dostęp do bazy danych Odzyskiwalny — reset przez narzędzia admin PostgreSQL
DB_ENCRYPTION_KEY Kody MFA, hasło SMTP, klucz API FX, pola PII przy ENCRYPT_PII=true Trwale nieczytelny — nie da się odszyfrować bez oryginalnego klucza
TIMING_TOKEN_SECRET Tokeny anty-timing-attack Regenerowalny — użytkownicy logują się ponownie
PUBLIC_LINK_PASSWORD_SESSION_SECRET Sesje linków publicznych Regenerowalny — aktywne sesje wygasają

Krytyczny sekret to DB_ENCRYPTION_KEY. Wszystkie inne sekrety można zregenerować z niewielkim utrudnieniem. Ale DB_ENCRYPTION_KEY szyfruje dane — utrata oznacza, że te pola przepadają na zawsze.

Odzyskiwanie po utracie .env

Jeśli stracisz .env, ale masz niezaszyfrowaną kopię zapasową bazy (domyślny tryb):

  1. Utwórz nowy .env z nowymi sekretami.
  2. Przywróć bazę z kopii zapasowej.
  3. Wszystkie dane biznesowe (klienci, oferty, produkty, użytkownicy) będą dostępne.
  4. Utracone: kody MFA (użytkownicy rejestrują się ponownie), hasło SMTP (wprowadź ponownie), klucz API FX. Jeśli ENCRYPT_PII=true było włączone, dane klientów też przepadają.

Kopie zapasowe

Stos zawiera osobny proces kopii zapasowych (backup-worker), tak jak na serwerze, więc kopie nigdy nie konkurują o pamięć z aplikacją WWW. Kopie planowe skonfigurujesz zgodnie z artykułem Kopie zapasowe i odtwarzanie; WORKER_MEM_LIMIT utrzymuj na poziomie co najmniej 768m.

Przejście z laptopa na serwer

Nie publikuj linków laptopowych klientom. Gdy będziesz gotowy na współdzielone lub publiczne wdrożenie, wybierz jedną ze ścieżek:

Last reviewed: Recently