Installation
Auf einem lokalen Laptop installieren
QuoteNode lokal zur Evaluierung ausführen, ohne es für Kunden zugänglich zu machen.
Auf einem lokalen Laptop installieren
Verwenden Sie diesen Pfad, wenn Sie QuoteNode auf einem Computer ausprobieren möchten. Es ist der schnellste Weg, die Anwendung zu evaluieren, aber keine kundenseitige Bereitstellung.
Wann Sie diesen Pfad verwenden
- Sie möchten eine Demo- oder Testinstanz.
- Sie testen Angebotserstellung, PDFs, Vorlagen, Produkte und Einstellungen.
- Kunden müssen keine öffentlichen Angebotslinks von einem anderen Gerät öffnen.
Was nicht funktionieren wird
- Öffentliche Angebotslinks, die als
http://localhost/...generiert werden, funktionieren nur auf demselben Laptop. - E-Mail-Links sind meist nicht nützlich, weil Empfänger
localhostnicht von ihren Geräten öffnen können. - HTTPS ist für diese Topologie normalerweise nicht konfiguriert.
- Der Laptop muss eingeschaltet sein und Docker muss laufen.
Voraussetzungen
- Docker Desktop oder Docker Engine mit Docker Compose v2.
- Mindestens 2,5 GB RAM, die für den Stack verfügbar sind.
- Mindestens 10 GB freier Festplattenspeicher.
- Ein 64-Bit-x86-Host (linux/amd64). ARM-Geräte, auch NAS-Modelle mit ARM-Prozessor, werden nicht unterstützt.
Überprüfen Sie Ihre Konfiguration:
docker --version # Should show 24.0+
docker compose version # Should show v2.x
Schritt 1 — Projektverzeichnis erstellen
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
Damit laden Sie das Bereitstellungspaket des Release v0.9.8-alpha herunter: docker-compose.yml mit per Digest festgelegten Images, env.example, release.json (die Release-Identität) und operator/ (Werkzeuge für Sicherung und Wiederherstellung).
Bekanntes Problem in v0.9.5-alpha: Ihre
env.exampleenthält eine aktive Beispielzeile,BACKUP_RCLONE_REMOTE=s3:mybucket/backups, durch die jedes Backup fehlschlägt. Die Einstellungen dieser Anleitung enthalten sie nicht; übernehmen Sie Einstellungen ausenv.example, lassen Sie diese Zeile weg (Details).
Schritt 2 — Konfigurationsdatei erstellen
Erstellen Sie eine Datei namens .env mit den minimalen Pflichteinstellungen. Generieren Sie Ihre eigenen Secrets — verwenden Sie die Platzhalterwerte nicht in der Produktion:
# 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
Speichern Sie Ihre
.env-Datei sofort. DerDB_ENCRYPTION_KEYverschlüsselt sensible Daten im Ruhezustand. Wenn Sie ihn verlieren, werden verschlüsselte Felder (MFA-Codes, SMTP-Passwort, API-Schlüssel) dauerhaft unlesbar. Kopieren Sie diese Datei an einen sicheren Ort.
Schritt 3 — Docker-Compose-Datei prüfen
Die docker-compose.yml aus dem Paket startet das Release v0.9.8-alpha mit per Digest festgelegten Images. Bearbeiten Sie sie nicht: Alle Einstellungen gehören in .env, und ein Upgrade ersetzt diese Datei durch die Version des nächsten Release. Zur Information:
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 legt fest, wie der Einstiegspunkt caddy QuoteNode ausliefert: Ein öffentlicher Hostname (crm.example.com) erhält automatisch ein HTTPS-Zertifikat; http://<Adresse> oder :80 liefert einfaches HTTP in einem vertrauenswürdigen Netz aus. Sind die Ports 80/443 belegt, setzen Sie HTTP_PORT / HTTPS_PORT in .env. Hinter Ihrem eigenen Reverse Proxy setzen Sie DOMAIN=:80 und tragen die Proxy-Adresse in EDGE_TRUSTED_PROXIES ein.
Schritt 4 — QuoteNode starten
docker compose up -d
Docker lädt die Container-Images herunter (beim ersten Start dauert das einige Minuten). Beobachten Sie den Startvorgang:
docker compose logs -f backend
Warten Sie, bis Sie Started QuoteNodeApplication sehen — das bedeutet, dass das Backend bereit ist.
Schritt 5 — QuoteNode öffnen
Navigieren Sie zu http://localhost in Ihrem Browser.
Standard-Anmeldedaten:
| Feld | Wert |
|---|---|
[email protected] |
|
| Passwort | Admin123! |
Ändern Sie das Standardpasswort sofort nach dem Einloggen (Einstellungen > Mein Profil > Passwort ändern).
Was läuft gerade?
Nach docker compose up laufen 6 Container:
| Container | Aufgabe | RAM-Verbrauch |
|---|---|---|
postgres |
Speichert alle Ihre Daten | ~256 MB |
backend |
Java-API-Server, Geschäftslogik | ~512 MB – 1,5 GB |
gotenberg |
Konvertiert HTML zu PDF (Chromium-basiert) | ~200 MB |
frontend |
Liefert die Weboberfläche aus | ~20 MB |
backup-worker |
Führt geplante Sicherungen aus (gleiches Image wie das Backend) | ~300 MB |
caddy |
Einstiegspunkt: HTTPS-Zertifikate, Routing, Sicherheits-Header | ~20 MB |
Gesamt: ungefähr 1,5 – 2,5 GB RAM.
Häufige Befehle
# 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
# Upgrade: docker-compose.yml durch die Datei des nächsten Release-Pakets ersetzen (siehe Upgrade-Anleitung)
Secrets sicher aufbewahren
Ihre .env-Datei enthält kryptografische Secrets, die einzigartig für Ihre Installation sind:
| Secret | Was es schützt | Bei Verlust |
|---|---|---|
DB_PASSWORD |
Datenbankzugang | Wiederherstellbar — über PostgreSQL-Admintool zurücksetzen |
DB_ENCRYPTION_KEY |
MFA-Codes, SMTP-Passwort, FX-API-Schlüssel und alle PII-Felder wenn ENCRYPT_PII=true |
Dauerhaft unlesbar — ohne den Originalschlüssel nicht entschlüsselbar |
TIMING_TOKEN_SECRET |
Anti-Timing-Angriff-Tokens | Neu generieren — bestehende Tokens ungültig, Benutzer authentifizieren sich neu |
PUBLIC_LINK_PASSWORD_SESSION_SECRET |
Sitzungen für öffentliche Links | Neu generieren — aktive Sitzungen laufen ab, Links funktionieren weiterhin |
Das kritische Secret ist
DB_ENCRYPTION_KEY. Alle anderen Secrets können mit kleinen Umständen neu generiert werden. AberDB_ENCRYPTION_KEYverschlüsselt Daten im Ruhezustand — sein Verlust bedeutet, dass diese Felder für immer weg sind.
Wiederherstellung bei Verlust von .env
Wenn Sie Ihre .env-Datei verlieren, aber ein unverschlüsseltes Datenbank-Backup haben (der Standard-Backup-Modus):
- Erstellen Sie eine neue
.envmit neuen Secrets. - Stellen Sie die Datenbank aus Ihrem Backup wieder her.
- Alle Geschäftsdaten (Kunden, Angebote, Produkte, Benutzer) sind vollständig zugänglich.
- Nur wenige Felder gehen verloren: MFA-Codes (Benutzer registrieren sich erneut), SMTP-Passwort (in den Einstellungen neu eingeben), FX-API-Schlüssel (in den Einstellungen neu eingeben). Wenn
ENCRYPT_PII=trueaktiviert war, gehen auch verschlüsselte Kundendaten verloren.
Backups
Der Stack enthält einen eigenen Sicherungsdienst (backup-worker), genau wie auf einem Server, sodass Sicherungen nie mit der Webanwendung um Speicher konkurrieren. Geplante Sicherungen richten Sie wie unter Sicherung und Wiederherstellung beschrieben ein; halten Sie WORKER_MEM_LIMIT bei mindestens 768m.
Vom Laptop auf einen Server umziehen
Veröffentlichen Sie keine Laptop-Links für Kunden. Wenn Sie bereit für eine gemeinsame oder öffentliche Bereitstellung sind, wählen Sie einen dieser Pfade: