Installation
Install on a Local Laptop
Run QuoteNode locally for evaluation without exposing it to clients.
Install on a Local Laptop
Use this path when you want to try QuoteNode on one computer. It is the fastest way to evaluate the application, but it is not a client-facing deployment.
When to use it
- You want a demo or trial instance.
- You are testing offer creation, PDFs, templates, products, and settings.
- You do not need customers to open public offer links from another device.
What will not work
- Public offer links generated as
http://localhost/...work only on the same laptop. - Email links are usually not useful because recipients cannot open
localhostfrom their machines. - HTTPS is normally not configured for this topology.
- The laptop must be awake and Docker must be running.
Prerequisites
- Docker Desktop or Docker Engine with Docker Compose v2.
- At least 2.5 GB RAM available for the stack.
- At least 10 GB free disk space.
Verify your setup:
docker --version # Should show 24.0+
docker compose version # Should show v2.x
Step 1 — Create a project directory
mkdir quotenode && cd quotenode
Step 2 — Create the configuration file
Create a file named .env with the minimum required settings. The secret values below are filled with fresh random values in your browser on each page load — use the ↻ New secrets button on the block to regenerate them, then copy the result. (Prefer to generate them yourself? See Deployment environment variables.)
# 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=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
Save your
.envfile immediately. TheDB_ENCRYPTION_KEYencrypts sensitive data at rest. If you lose it, encrypted fields (MFA codes, SMTP password, API keys) become permanently unreadable. Copy this file to a safe location.
Step 3 — Create the Docker Compose file
Create a file named docker-compose.yml:
services:
postgres:
image: postgres:18-alpine
cpus: 2.0
mem_limit: 384m
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
PGDATA: /var/lib/postgresql/data # required for the postgres:18 image
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME}"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
gotenberg:
image: gotenberg/gotenberg:8
cpus: 2.0
mem_limit: 384m
environment:
LOG_LEVEL: info
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
backend:
image: ghcr.io/lesisty7/quotenode/quotenode-api:latest
cpus: 2.0
mem_limit: 1280m
depends_on:
postgres: { condition: service_healthy }
gotenberg: { condition: service_healthy }
env_file: .env
environment:
SPRING_PROFILES_ACTIVE: prod
# Single-container eval (WEB_MAINTENANCE backup profile): the backend runs
# scheduled backups in-process, so no separate backup-worker container is needed.
# For production, split into a web backend + a dedicated backup-only worker.
JOBS_MODE: all
BACKUP_RUNTIME_PROFILE: WEB_MAINTENANCE
PDF_ENABLED: "true"
PDF_GOTENBERG_URL: http://gotenberg:3000
volumes:
- backend_uploads:/app/data/uploads
- backend_pdfs:/app/data/pdfs
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
interval: 15s
timeout: 10s
retries: 5
start_period: 45s
restart: unless-stopped
frontend:
image: ghcr.io/lesisty7/quotenode/quotenode-frontend:latest
cpus: 1.0
mem_limit: 128m
depends_on:
backend: { condition: service_healthy }
ports:
- "80:80"
- "443:443"
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:80/"]
interval: 15s
timeout: 5s
retries: 3
restart: unless-stopped
volumes:
postgres_data:
backend_uploads:
backend_pdfs:
Step 4 — Start QuoteNode
docker compose up -d
Docker will download the container images (this takes a few minutes on the first run). Watch the startup progress:
docker compose logs -f backend
Wait until you see Started QuoteNodeApplication — this means the backend is ready.
Step 5 — Open QuoteNode
Navigate to http://localhost in your browser.
Default login credentials:
| Field | Value |
|---|---|
[email protected] | |
| Password | Admin123! |
Change the default password immediately after logging in (Settings > My Profile > Change Password).
What is running?
After docker compose up, you have 4 containers:
| Container | What it does | RAM usage |
|---|---|---|
postgres | Stores all your data | ~256 MB |
backend | Java API server, business logic | ~512 MB – 1.5 GB |
gotenberg | Converts HTML to PDF (Chromium-based) | ~200 MB |
frontend | Serves the web UI + reverse proxy | ~20 MB |
Total: approximately 1.5 – 2.5 GB RAM.
Common commands
# 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
# Update to the latest version
docker compose pull && docker compose up -d
Keeping your secrets safe
Your .env file contains cryptographic secrets that are unique to your installation:
| Secret | What it protects | If lost |
|---|---|---|
DB_PASSWORD | Database access | Recoverable — reset via PostgreSQL admin tools |
DB_ENCRYPTION_KEY | MFA codes, SMTP password, FX API key, and all PII fields when ENCRYPT_PII=true | Permanently unreadable — cannot be decrypted without the original key |
TIMING_TOKEN_SECRET | Anti-timing-attack tokens | Regenerate — existing tokens invalidated, users re-authenticate |
PUBLIC_LINK_PASSWORD_SESSION_SECRET | Public link sessions | Regenerate — active sessions expire, links still work |
The critical secret is
DB_ENCRYPTION_KEY. All other secrets can be regenerated with minor inconvenience. ButDB_ENCRYPTION_KEYencrypts data at rest — losing it means those fields are gone forever.
Recovery if .env is lost
If you lose your .env file but have an unencrypted database backup (the default backup mode):
- Create a fresh
.envwith new secrets. - Restore the database from your backup.
- All business data (customers, offers, products, users) will be fully accessible.
- Only a few fields are lost: MFA codes (users re-enroll), SMTP password (re-enter in settings), FX API key (re-enter in settings). If
ENCRYPT_PII=truewas enabled, encrypted customer data is also lost.
Backups
This single-container setup runs scheduled backups inside the backend process (JOBS_MODE: all), so you do not need a separate backup-worker container. The backup itself runs as a shell pipeline (pg_dump → compress → encrypt) outside the JVM, so its memory cost is small — this is why the laptop backend is sized at 1280m rather than 1g.
This is acceptable for a laptop or evaluation instance. For a production deployment, run a dedicated backup-only worker container (JOBS_MODE: backup-only) alongside a web backend (JOBS_MODE: web) so backups never compete with web traffic for memory. See Backup & Recovery.
Moving from laptop to server
Do not publish laptop links to customers. When you are ready for a shared or public deployment, choose one of these paths: