Installation
Install on an Ubuntu Public Server
Deploy QuoteNode on a public Ubuntu server with a domain, HTTPS, SMTP, backups, and firewall basics.
Install on an Ubuntu Public Server
Use this path for a production deployment reachable by customers through a public domain.
When to use it
- External customers must open public offer links.
- Email messages should contain working links.
- You want direct control over the server instead of a PaaS layer.
What will not work without proper setup
- Public links fail if DNS does not point to the server.
- Public links fail if the browser URL differs from CORS configuration.
- HTTPS certificate issuance fails if ports 80/443 are blocked.
- Email links fail if SMTP is missing or if messages are rejected by the recipient’s mail provider.
- Dynamic home IP deployments need DDNS and router port forwarding; a VPS is usually simpler.
Prerequisites
- Ubuntu LTS server or VPS.
- Public DNS record, for example
crm.example.com, pointing to the server. - Ports 80 and 443 open from the internet.
- Docker Engine 24.0+ and Docker Compose v2.
- SMTP account for email delivery.
- At least 4 GB RAM and 20 GB free disk space (recommended for production).
- Backup destination and a restore procedure.
Verify Docker:
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. Replace crm.example.com with your real domain. 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 — UNIQUE to this installation.
# 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
# Public domain — replace with your real domain
CORS_ALLOWED_ORIGINS=https://crm.example.com
DOMAIN=crm.example.com
# Email delivery
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME[email protected]
SMTP_PASSWORD=your-smtp-password
SMTP_AUTH=true
SMTP_STARTTLS=true
# Notification preference links (must match your public domain)
NOTIFICATIONS_PUBLIC_PREFERENCES_URL=https://crm.example.com/notifications/preferences
# Application
LOG_LEVEL=INFO
SPRING_PROFILES_ACTIVE=prod
# 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=
Back up your
.envfile before first production use. Store it in a password manager or a physical safe. TheDB_ENCRYPTION_KEYis critical — losing it means encrypted data is gone forever.
SMTP alternative: You can skip the SMTP_* variables from .env and configure email later in Settings > Email SMTP inside QuoteNode. The admin panel stores the SMTP password encrypted with DB_ENCRYPTION_KEY, keeping it out of your .env file.
Step 3 — Create the Docker Compose file
Create a file named docker-compose.yml:
services:
postgres:
image: postgres:18-alpine
cpus: 4.0
mem_limit: 512m
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: 448m
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: 4.0
mem_limit: 1792m
depends_on:
postgres: { condition: service_healthy }
gotenberg: { condition: service_healthy }
env_file: .env
environment:
SPRING_PROFILES_ACTIVE: prod
JOBS_MODE: web
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: 2.0
mem_limit: 192m
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 — Configure the firewall
Open only what is needed:
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
Do not expose PostgreSQL publicly. It stays on the Docker network.
Step 5 — Start QuoteNode
docker compose up -d
Watch the startup:
docker compose logs -f backend
Wait until you see Started QuoteNodeApplication. The frontend container (Caddy) will automatically obtain a Let’s Encrypt certificate for your domain.
Step 6 — Open QuoteNode
Navigate to https://crm.example.com in your browser.
Default login credentials:
| Field | Value |
|---|---|
[email protected] | |
| Password | Admin123! |
Change the default password immediately after logging in.
Post-deploy checks
- Login and change the admin password.
- Create a test offer.
- Generate a PDF.
- Create and open a public link from another device.
- If SMTP is configured, send a test notification and verify the email links work.
What is running?
| 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 (Caddy with auto-HTTPS) | ~20 MB |
Total: approximately 1.5 – 2.5 GB RAM.
Common commands
# Check service status
docker compose ps
# View logs
docker compose logs -f backend
# Stop QuoteNode
docker compose down
# Update to the latest version
docker compose pull && docker compose up -d
Upgrade checklist
Before every upgrade:
- Confirm the current app version in the admin area.
- Back up
.env. - Run or verify a fresh database backup.
- Pull the new images and restart.
- Check backend health, login, PDF generation, and a public offer link.
Keeping your secrets safe
| 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, PII fields | Permanently unreadable |
TIMING_TOKEN_SECRET | Anti-timing-attack tokens | Regenerate — users re-authenticate |
PUBLIC_LINK_PASSWORD_SESSION_SECRET | Public link sessions | Regenerate — active sessions expire |
Store
.envin a password manager (1Password, Bitwarden). For maximum safety, print the secrets and keep the printout in a physical safe. Never commit.envto version control.
Recovery if .env is lost
If you lose your .env file but have an unencrypted database backup (the default):
- Create a fresh
.envwith new secrets. - Restore the database from your backup.
- All business data will be accessible.
- Lost: MFA codes (users re-enroll), SMTP password (re-enter), FX API key (re-enter). If
ENCRYPT_PII=truewas enabled, encrypted customer data is also lost.