Skip to content
Q
QuoteNode

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 .env file before first production use. Store it in a password manager or a physical safe. The DB_ENCRYPTION_KEY is 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:

FieldValue
Email[email protected]
PasswordAdmin123!

Change the default password immediately after logging in.

Post-deploy checks

  1. Login and change the admin password.
  2. Create a test offer.
  3. Generate a PDF.
  4. Create and open a public link from another device.
  5. If SMTP is configured, send a test notification and verify the email links work.

What is running?

ContainerWhat it doesRAM usage
postgresStores all your data~256 MB
backendJava API server, business logic~512 MB – 1.5 GB
gotenbergConverts HTML to PDF (Chromium-based)~200 MB
frontendServes 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:

  1. Confirm the current app version in the admin area.
  2. Back up .env.
  3. Run or verify a fresh database backup.
  4. Pull the new images and restart.
  5. Check backend health, login, PDF generation, and a public offer link.

Keeping your secrets safe

SecretWhat it protectsIf lost
DB_PASSWORDDatabase accessRecoverable — reset via PostgreSQL admin tools
DB_ENCRYPTION_KEYMFA codes, SMTP password, FX API key, PII fieldsPermanently unreadable
TIMING_TOKEN_SECRETAnti-timing-attack tokensRegenerate — users re-authenticate
PUBLIC_LINK_PASSWORD_SESSION_SECRETPublic link sessionsRegenerate — active sessions expire

Store .env in a password manager (1Password, Bitwarden). For maximum safety, print the secrets and keep the printout in a physical safe. Never commit .env to version control.

Recovery if .env is lost

If you lose your .env file but have an unencrypted database backup (the default):

  1. Create a fresh .env with new secrets.
  2. Restore the database from your backup.
  3. All business data will be accessible.
  4. Lost: MFA codes (users re-enroll), SMTP password (re-enter), FX API key (re-enter). If ENCRYPT_PII=true was enabled, encrypted customer data is also lost.

Last reviewed: Recently