Table of Contents
1. Overview & Architecture
In a two-node deployment, the application and database run on separate machines. This provides:
- Better resource isolation — CPU/memory for Node.js vs. PostgreSQL don't compete
- Independent scaling — upgrade database hardware without touching the app server
- Enhanced security — the database server can be on a private subnet, unreachable from the internet
- Simplified backups — database backups happen on the DB server without impacting app performance
- High availability — with a read replica, you can failover if the primary database goes down
Architecture Diagram
Node Roles Summary
Application Server
- Node.js 20 LTS runtime
- MinusNow Express.js app
- React 19 frontend (served by Express)
- Listens on port
5000 - Connects to remote PostgreSQL
- No local database needed
Primary Database
- PostgreSQL 15+ server
- Listens on port
5432 - Accepts connections from Node 1 IP
- Stores all application data
- Handles WAL archiving
- Streams to replica (optional)
Read Replica (Optional)
- PostgreSQL 15+ standby
- Receives WAL stream from primary
- Read-only queries / reporting
- Can be promoted to primary
- Hot standby for failover
2. Prerequisites
Hardware Requirements
| Requirement | Node 1 (App Server) | Node 2 (DB Server) |
|---|---|---|
| CPU | 2+ cores | 2+ cores (4 recommended) |
| RAM | 4 GB minimum (8 recommended) | 4 GB minimum (8+ recommended) |
| Disk | 20 GB (SSD recommended) | 50 GB+ SSD (for data + WAL) |
| Network | Gigabit Ethernet | Gigabit Ethernet |
| OS | Windows Server 2019+ / Ubuntu 20.04+ / RHEL 8+ | Same OS options |
Software Requirements
Required Software
- Node.js 20 LTS (includes npm)
- Git (to clone source code)
- MinusNow source code or artifact ZIP
- psql client (to test DB connection — optional)
Node 1 does NOT need PostgreSQL installed. The app connects to Node 2's database over the network. You only need psql if you want to test the connection manually.
Required Software
- PostgreSQL 15+ (15, 16, 17, or 18)
- pg_hba.conf editing access (admin)
- postgresql.conf editing access (admin)
Node 2 does NOT need Node.js. This machine only runs PostgreSQL. It doesn't need any JavaScript tooling.
Network Requirements
- Both nodes can reach each other over TCP (same LAN, VLAN, or VPN)
- Node 1 can reach Node 2 on port
5432(PostgreSQL) - Firewall on Node 2 allows inbound TCP 5432 from Node 1's IP
- If using a replica: replica can reach primary on port
5432 - DNS or static IPs assigned to each node
3. Network Planning
Before starting installation, plan your IP addresses and hostnames. Use this table as a template:
| Role | Hostname | IP Address | OS | Ports |
|---|---|---|---|---|
| Node 1 App Server | minusnow-app | 192.168.1.10 | Your choice | 5000 (HTTP) |
| Node 2 DB Primary | minusnow-db | 192.168.1.20 | Your choice | 5432 (PostgreSQL) |
| Replica DB Standby | minusnow-db-replica | 192.168.1.30 | Your choice | 5432 (PostgreSQL) |
Replace the example IPs (192.168.1.10, 192.168.1.20, 192.168.1.30) with your actual network IPs throughout this guide. These IPs are used as placeholders.
Step 3a: Verify Network Connectivity
From Node 1, verify you can reach Node 2:
PowerShell# Ping the DB server
ping 192.168.1.20
# Test TCP port 5432 connectivity (after PostgreSQL is configured)
Test-NetConnection -ComputerName 192.168.1.20 -Port 5432
Bash# Ping the DB server
ping -c 4 192.168.1.20
# Test TCP port 5432 connectivity (after PostgreSQL is configured)
nc -zv 192.168.1.20 5432
Step 3b: Set Up Hostname Resolution (Optional but Recommended)
Adding hostnames to /etc/hosts (Linux) or C:\Windows\System32\drivers\etc\hosts (Windows) makes configuration cleaner:
Open Notepad as Administrator → File → Open → C:\Windows\System32\drivers\etc\hosts
hosts# MinusNow Two-Node Deployment
192.168.1.10 minusnow-app
192.168.1.20 minusnow-db
192.168.1.30 minusnow-db-replica
Bash# Add to /etc/hosts on BOTH nodes
sudo tee -a /etc/hosts <<EOF
# MinusNow Two-Node Deployment
192.168.1.10 minusnow-app
192.168.1.20 minusnow-db
192.168.1.30 minusnow-db-replica
EOF
4. Node 2: Primary Database Setup
Why Node 2 first? We set up the database server before the application server because Node 1 needs a running database to connect to during setup.
Step 4a: Install PostgreSQL
Download PostgreSQL Installer
Go to postgresql.org/download/windows and download PostgreSQL 15, 16, 17, or 18.
Run the Installer
- Set a strong password for the
postgressuperuser (write it down) - Keep the default port
5432 - Accept default data directory (e.g.,
C:\Program Files\PostgreSQL\18\data)
Add PostgreSQL to PATH
The installer often doesn't add the bin folder to PATH. Fix this:
PowerShell (Admin)# Adjust the version number to match your installation (15, 16, 17, or 18)
$pgBin = "C:\Program Files\PostgreSQL\18\bin"
# Add to system PATH permanently
$currentPath = [Environment]::GetEnvironmentVariable("Path", "Machine")
if ($currentPath -notlike "*$pgBin*") {
[Environment]::SetEnvironmentVariable("Path", "$currentPath;$pgBin", "Machine")
Write-Host "PostgreSQL bin added to PATH. Close and reopen PowerShell."
} else {
Write-Host "PostgreSQL bin already in PATH."
}
# Verify (in a NEW PowerShell window)
psql --version
Verify PostgreSQL is Running
PowerShell# Check service status
Get-Service -Name "postgresql*"
# Should show: Running
Ubuntu / Debian
Bash# Install PostgreSQL
sudo apt update
sudo apt install -y postgresql postgresql-contrib
# Start and enable
sudo systemctl start postgresql
sudo systemctl enable postgresql
# Verify
sudo systemctl status postgresql
psql --version
RHEL / CentOS / Rocky Linux
Bash# Install PostgreSQL 15 module
sudo dnf module enable postgresql:15
sudo dnf install -y postgresql-server postgresql-contrib
# Initialize and start
sudo postgresql-setup --initdb
sudo systemctl start postgresql
sudo systemctl enable postgresql
# Verify
sudo systemctl status postgresql
Step 4b: Create the MinusNow Database and User
PowerShell# Connect as superuser
psql -U postgres
# Inside psql, run these commands:
CREATE DATABASE minusnow;
CREATE USER minusnow_user WITH ENCRYPTED PASSWORD 'YourStrongPassword123!';
GRANT ALL PRIVILEGES ON DATABASE minusnow TO minusnow_user;
ALTER DATABASE minusnow OWNER TO minusnow_user;
-- PostgreSQL 15+ requires explicit schema permissions
\c minusnow
GRANT ALL ON SCHEMA public TO minusnow_user;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO minusnow_user;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO minusnow_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO minusnow_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO minusnow_user;
\q
Replace YourStrongPassword123! with a real strong password. This password will be used in Node 1's DATABASE_URL environment variable. Use a mix of uppercase, lowercase, numbers, and symbols. Avoid @, #, / characters as they can break the connection URL.
Bash# Switch to postgres user and create database
sudo -u postgres psql <<SQL
CREATE DATABASE minusnow;
CREATE USER minusnow_user WITH ENCRYPTED PASSWORD 'YourStrongPassword123!';
GRANT ALL PRIVILEGES ON DATABASE minusnow TO minusnow_user;
ALTER DATABASE minusnow OWNER TO minusnow_user;
SQL
# Grant schema permissions (required for PostgreSQL 15+)
sudo -u postgres psql -d minusnow <<SQL
GRANT ALL ON SCHEMA public TO minusnow_user;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO minusnow_user;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO minusnow_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO minusnow_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO minusnow_user;
SQL
Step 4c: Configure PostgreSQL to Accept Remote Connections
This is the critical step. By default, PostgreSQL only listens on localhost (127.0.0.1). For Node 1 to connect over the network, you must configure PostgreSQL to listen on all interfaces (or the specific IP) and allow the connection in pg_hba.conf.
Step 4c-i: Edit postgresql.conf
Find and edit the postgresql.conf file:
| OS | Typical Location |
|---|---|
| Windows | C:\Program Files\PostgreSQL\18\data\postgresql.conf |
| Ubuntu/Debian | /etc/postgresql/15/main/postgresql.conf |
| RHEL/CentOS | /var/lib/pgsql/data/postgresql.conf |
Find the listen_addresses line and change it:
postgresql.conf# BEFORE (default — only localhost)
#listen_addresses = 'localhost'
# AFTER — listen on all interfaces
listen_addresses = '*'
# OR — listen on specific IP only (more secure)
listen_addresses = '192.168.1.20'
Explanation:
'*'= listen on ALL network interfaces (easiest but less secure)'192.168.1.20'= listen ONLY on this specific IP (more secure)'localhost,192.168.1.20'= listen on localhost AND the specific IP (recommended)
Step 4c-ii: Edit pg_hba.conf
Find and edit the pg_hba.conf file (same directory as postgresql.conf):
| OS | Typical Location |
|---|---|
| Windows | C:\Program Files\PostgreSQL\18\data\pg_hba.conf |
| Ubuntu/Debian | /etc/postgresql/15/main/pg_hba.conf |
| RHEL/CentOS | /var/lib/pgsql/data/pg_hba.conf |
Add these lines at the end of the file:
pg_hba.conf# Allow Node 1 (App Server) to connect to MinusNow database
# TYPE DATABASE USER ADDRESS METHOD
host minusnow minusnow_user 192.168.1.10/32 scram-sha-256
# If you have a replica, also allow it:
host replication replicator 192.168.1.30/32 scram-sha-256
Understanding pg_hba.conf entries:
| Field | Value | Meaning |
|---|---|---|
host | — | TCP/IP connection (not Unix socket) |
minusnow | Database name | Only allow connection to this database |
minusnow_user | Username | Only this user can connect |
192.168.1.10/32 | CIDR address | /32 = exact IP match (most secure) |
scram-sha-256 | Auth method | Password-based authentication (encrypted) |
Do NOT use trust as the method — that allows connections without a password!
Step 4c-iii: Restart PostgreSQL
PowerShell (Admin)# Restart PostgreSQL service
Restart-Service -Name "postgresql*"
# Verify it's running
Get-Service -Name "postgresql*"
Bash# Restart PostgreSQL
sudo systemctl restart postgresql
# Verify it's running
sudo systemctl status postgresql
# Check it's listening on the right address
sudo ss -tlnp | grep 5432
# Should show: 0.0.0.0:5432 or 192.168.1.20:5432
5. Node 2: Firewall Configuration
The database server's firewall must allow incoming connections on port 5432 from the application server. Only allow the specific Node 1 IP — never open port 5432 to the entire network.
PowerShell (Admin)# Allow PostgreSQL port from Node 1 only
New-NetFirewallRule `
-DisplayName "MinusNow - PostgreSQL from App Server" `
-Direction Inbound `
-Protocol TCP `
-LocalPort 5432 `
-RemoteAddress 192.168.1.10 `
-Action Allow `
-Profile Domain,Private
# Verify the rule
Get-NetFirewallRule -DisplayName "MinusNow*" | Format-Table Name, DisplayName, Enabled
The -RemoteAddress flag restricts this rule to only allow connections from Node 1's IP address. This is much more secure than allowing all IPs.
Using UFW (Ubuntu/Debian)
Bash# Allow PostgreSQL from Node 1 ONLY
sudo ufw allow from 192.168.1.10 to any port 5432 proto tcp comment "MinusNow App Server"
# If using a replica, also allow it:
sudo ufw allow from 192.168.1.30 to any port 5432 proto tcp comment "MinusNow DB Replica"
# Verify
sudo ufw status numbered
Using firewalld (RHEL/CentOS)
Bash# Create a rich rule for Node 1 only
sudo firewall-cmd --permanent --add-rich-rule='
rule family="ipv4"
source address="192.168.1.10/32"
port protocol="tcp" port="5432"
accept'
# For replica:
sudo firewall-cmd --permanent --add-rich-rule='
rule family="ipv4"
source address="192.168.1.30/32"
port protocol="tcp" port="5432"
accept'
sudo firewall-cmd --reload
sudo firewall-cmd --list-rich-rules
Step 5b: Test the Firewall from Node 1
After configuring the firewall, go to Node 1 and test connectivity:
PowerShell# Test TCP connectivity to PostgreSQL port
Test-NetConnection -ComputerName 192.168.1.20 -Port 5432
# Expected output:
# TcpTestSucceeded : True
Bash# Test TCP connectivity
nc -zv 192.168.1.20 5432
# Expected: Connection to 192.168.1.20 5432 port [tcp/postgresql] succeeded!
6. Node 1: Application Server Setup
Step 6a: Install Node.js 20 LTS
Download Node.js
Go to nodejs.org and download Node.js 20 LTS (Windows Installer .msi).
Verify Installation
PowerShellnode --version # Should show v20.x.x
npm --version # Should show 10.x.x
Bash# Ubuntu/Debian
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
# RHEL/CentOS
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
sudo dnf install -y nodejs
# Verify
node --version # Should show v20.x.x
npm --version # Should show 10.x.x
Step 6b: Deploy MinusNow Application Code
Option A: From source code ZIP (downloaded from Download Center)
PowerShell# Create project directory
mkdir D:\MinusNow
cd D:\MinusNow
# Extract the source code ZIP
Expand-Archive -Path ".\minusnow-itsm-source.zip" -DestinationPath "." -Force
Option B: From Git repository
PowerShellcd D:\
git clone <your-repo-url> MinusNow
cd MinusNow
Option A: From source code archive
Bash# Create project directory
sudo mkdir -p /opt/minusnow
sudo chown $USER:$USER /opt/minusnow
cd /opt/minusnow
# Extract (if .zip)
unzip minusnow-itsm-source.zip -d .
Option B: From Git
Bashcd /opt
sudo git clone <your-repo-url> minusnow
sudo chown -R $USER:$USER /opt/minusnow
cd /opt/minusnow
Step 6c: Install Dependencies
Both platforms# Navigate to project directory
cd D:\MinusNow # Windows
cd /opt/minusnow # Linux
# Clean install (removes any stale global packages)
npm install
Global drizzle-kit conflict: If you have an old version of drizzle-kit installed globally, it will override the local one and cause npm run db:push to fail with "unknown command 'push'". Fix:
PowerShell / Bash# Check for global drizzle-kit
npm list -g drizzle-kit
# If found, remove it
npm uninstall -g drizzle-kit
# Use local version (installed via npm install)
npx drizzle-kit --version # Should show 0.31.x
7. Node 1: Connect App to Remote Database
This is the most important step. The DATABASE_URL environment variable tells the MinusNow app where to find the PostgreSQL database. In a two-node setup, this URL points to Node 2's IP address instead of localhost.
Step 7a: Understand the DATABASE_URL Format
The connection URL follows this format:
Formatpostgresql://USERNAME:PASSWORD@HOST:PORT/DATABASE
│ │ │ │ │
│ │ │ │ └─ Database name: minusnow
│ │ │ └─────── Port: 5432 (default)
│ │ └───────────── Node 2's IP: 192.168.1.20
│ └───────────────────── Password you created in Step 4b
└────────────────────────────── Username: minusnow_user
Single-Node vs Two-Node Comparison
| Deployment | DATABASE_URL |
|---|---|
| Single-Node (same machine) | postgresql://minusnow_user:Pass@localhost:5432/minusnow |
| Two-Node (remote DB) | postgresql://minusnow_user:Pass@192.168.1.20:5432/minusnow |
Step 7b: Create the .env File on Node 1
PowerShellcd D:\MinusNow
# Generate a random SESSION_SECRET
$secret = -join ((65..90)+(97..122)+(48..57) | Get-Random -Count 48 | ForEach-Object {[char]$_})
# Create .env file — NOTE: Host is Node 2's IP, NOT localhost!
@"
DATABASE_URL=postgresql://minusnow_user:YourStrongPassword123!@192.168.1.20:5432/minusnow
SESSION_SECRET=$secret
PORT=5000
NODE_ENV=development
"@ | Out-File -Encoding utf8 .env
# Verify the file
Get-Content .env
Bashcd /opt/minusnow
# Generate a random SESSION_SECRET
SECRET=$(openssl rand -base64 36)
# Create .env file — NOTE: Host is Node 2's IP, NOT localhost!
cat > .env <<EOF
DATABASE_URL=postgresql://minusnow_user:YourStrongPassword123!@192.168.1.20:5432/minusnow
SESSION_SECRET=$SECRET
PORT=5000
NODE_ENV=development
EOF
# Secure the file
chmod 600 .env
# Verify
cat .env
Common Mistakes in DATABASE_URL:
- ✗ Using
localhost— there is no database on Node 1! - ✗ Wrong password — copy exactly from Step 4b
- ✗ Forgetting the port
:5432 - ✗ Using
@or#characters in the password (breaks URL parsing) - ✓
postgresql://minusnow_user:YourStrongPassword123!@192.168.1.20:5432/minusnow
Step 7c: Test Database Connection from Node 1
Before pushing the schema, verify the connection works:
PowerShell# If you have psql installed on Node 1 (optional):
psql -h 192.168.1.20 -U minusnow_user -d minusnow -c "SELECT version();"
# If psql is NOT installed, use Node.js to test:
node -e "
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL || 'postgresql://minusnow_user:YourStrongPassword123!@192.168.1.20:5432/minusnow' });
pool.query('SELECT NOW() as time, version() as ver')
.then(r => { console.log('Connected!', r.rows[0]); pool.end(); })
.catch(e => { console.error('FAILED:', e.message); pool.end(); });
"
Bash# Test with psql (if available):
PGPASSWORD='YourStrongPassword123!' psql -h 192.168.1.20 -U minusnow_user -d minusnow -c "SELECT version();"
# Or test with Node.js:
node -e "
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
pool.query('SELECT NOW() as time')
.then(r => { console.log('Connected!', r.rows[0]); pool.end(); })
.catch(e => { console.error('FAILED:', e.message); pool.end(); });
"
If the connection fails, check these items in order:
- PostgreSQL is running on Node 2 (
Get-Service postgresql*/systemctl status postgresql) listen_addressesinpostgresql.confis set to'*'or includes Node 2's IPpg_hba.confhas an entry allowing Node 1's IP- Firewall on Node 2 allows port 5432 from Node 1
- Password is correct and doesn't contain URL-breaking characters
Step 7d: Push Database Schema
Once the connection is verified, push the MinusNow schema to the remote database:
Both platforms# Push schema to remote PostgreSQL on Node 2
npm run db:push
If successful, you'll see drizzle-kit output showing the tables being created on Node 2's database. The schema is identical whether the database is local or remote.
Step 7e: Build and Start the Application
Both platforms# Build for production
npm run build
# Start the application
npm run start
# Or for development mode:
npm run dev
The application should now be running on http://NODE1_IP:5000 and connecting to PostgreSQL on Node 2.
8. Verify End-to-End Connectivity
After starting the application, verify that everything is working across both nodes:
Step 8a: Check Application Health
From any machine# Health check endpoint
curl http://192.168.1.10:5000/api/health
# Expected response:
# {"status":"healthy","timestamp":"2026-02-13T...","uptime":...}
Step 8b: Verify Database Connectivity from App
From Node 1# Check that the app can query the database
curl http://localhost:5000/api/health
# The health check should return "healthy" — this confirms:
# 1. Node.js is running on Node 1
# 2. Express is serving on port 5000
# 3. PostgreSQL connection to Node 2 is working
# 4. Database schema is properly set up
Step 8c: Open in Browser
Navigate to http://192.168.1.10:5000 (or http://minusnow-app:5000 if you set up hostnames) in your browser. You should see the MinusNow login page.
Step 8d: Verify from Node 2 (Database Server)
On Node 2, verify the application has connected and created tables:
Node 2# Connect to the database
# Windows: psql -U minusnow_user -d minusnow
# Linux: sudo -u postgres psql -d minusnow
# List all tables created by MinusNow
\dt
# Check active connections from Node 1
SELECT client_addr, state, query
FROM pg_stat_activity
WHERE datname = 'minusnow' AND client_addr IS NOT NULL;
Success indicators:
\dtshows MinusNow tables (users, tickets, incidents, etc.)pg_stat_activityshows connections from192.168.1.10(Node 1)- Browser shows the MinusNow login page
- Health endpoint returns
"healthy"
9. Replica Database Setup (Optional)
When do you need a replica?
- High availability — automatic failover if the primary goes down
- Read scaling — offload read-heavy reporting queries to the replica
- Disaster recovery — an up-to-date copy on a separate machine/location
- Zero-downtime upgrades — promote replica, upgrade old primary, re-add as replica
If you don't need these features, skip to Section 12: Monitoring.
Step 9a: Prepare the Replica Machine
On the replica machine (IP: 192.168.1.30), install PostgreSQL — same version as the primary:
Install PostgreSQL using the same installer as Node 2. Stop the PostgreSQL service after installation — we'll configure it as a standby before starting.
PowerShell (Admin)# Stop PostgreSQL (we need to replace its data directory)
Stop-Service -Name "postgresql*"
Bash# Install same PostgreSQL version
sudo apt install -y postgresql postgresql-contrib # Ubuntu/Debian
# OR
sudo dnf install -y postgresql-server # RHEL/CentOS
# Stop PostgreSQL — we'll configure it as standby
sudo systemctl stop postgresql
Step 9b: Create Replication User on Primary (Node 2)
On Node 2 (primary), create a dedicated user for replication:
Node 2 — psql# Connect as superuser
# Windows: psql -U postgres
# Linux: sudo -u postgres psql
CREATE ROLE replicator WITH REPLICATION LOGIN ENCRYPTED PASSWORD 'ReplicaPassword456!';
# Verify
\du replicator
Make sure you already added the pg_hba.conf line for the replica in Section 4c-ii. If not, add it now and restart PostgreSQL on the primary.
10. PostgreSQL Streaming Replication
How Streaming Replication Works
Step 10a: Configure Primary for Replication (Node 2)
Edit postgresql.conf on Node 2:
postgresql.conf (Node 2)# ---- Replication Settings ----
wal_level = replica # Required for streaming replication
max_wal_senders = 5 # Max number of replicas that can connect
wal_keep_size = 1024 # Keep 1 GB of WAL for replicas (MB)
hot_standby = on # Allow read queries on standby
# Optional: Synchronous replication (higher safety, lower performance)
# synchronous_standby_names = 'minusnow_replica'
Restart PostgreSQL on Node 2 after saving:
Node 2# Windows:
Restart-Service -Name "postgresql*"
# Linux:
sudo systemctl restart postgresql
Step 10b: Create Base Backup on Replica
On the replica machine, use pg_basebackup to clone the primary's data:
PowerShell (Admin)# Stop PostgreSQL if running
Stop-Service -Name "postgresql*"
# Remove the default data directory contents
$dataDir = "C:\Program Files\PostgreSQL\18\data"
Remove-Item "$dataDir\*" -Recurse -Force
# Create base backup from primary
pg_basebackup -h 192.168.1.20 -U replicator -D "$dataDir" -P -Xs -R
# The -R flag creates standby.signal and sets primary_conninfo automatically!
# Verify standby.signal was created
Test-Path "$dataDir\standby.signal" # Should be True
# Start PostgreSQL
Start-Service -Name "postgresql*"
Bash# Stop PostgreSQL
sudo systemctl stop postgresql
# Remove existing data directory contents
# Ubuntu/Debian: data is in /var/lib/postgresql/15/main
# RHEL/CentOS: data is in /var/lib/pgsql/data
DATA_DIR="/var/lib/postgresql/15/main" # Adjust for your OS/version
sudo rm -rf $DATA_DIR/*
# Create base backup from primary (as postgres user)
sudo -u postgres pg_basebackup \
-h 192.168.1.20 \
-U replicator \
-D $DATA_DIR \
-P -Xs -R
# The -R flag automatically creates:
# - standby.signal (tells PostgreSQL this is a standby)
# - Adds primary_conninfo to postgresql.auto.conf
# Verify
sudo ls -la $DATA_DIR/standby.signal # Should exist
# Start PostgreSQL
sudo systemctl start postgresql
# Check replication status
sudo -u postgres psql -c "SELECT * FROM pg_stat_wal_receiver;"
What the -R flag does:
- Creates
standby.signalfile in the data directory - Adds
primary_conninfo = 'host=192.168.1.20 user=replicator password=...'topostgresql.auto.conf - This tells PostgreSQL to start in standby mode and stream WAL from the primary
Step 10c: Verify Replication is Working
On the Primary (Node 2) — check that the replica is connected:
Node 2 — psqlSELECT client_addr, state, sent_lsn, write_lsn, flush_lsn, replay_lsn
FROM pg_stat_replication;
-- Expected output:
-- client_addr | state | sent_lsn | write_lsn | ...
-- ----------------+-----------+-------------+-------------+-----
-- 192.168.1.30 | streaming | 0/3000D60 | 0/3000D60 | ...
On the Replica — check replication status:
Replica — psqlSELECT status, received_lsn, latest_end_lsn
FROM pg_stat_wal_receiver;
-- Expected: status = 'streaming'
-- Verify it's in recovery mode:
SELECT pg_is_in_recovery();
-- Expected: t (true)
Step 10d: Test Read-Only Queries on Replica
Replica — psql# Connect to the minusnow database on the replica
psql -U minusnow_user -d minusnow -h 192.168.1.30
# Run a read query — should work:
SELECT count(*) FROM users;
# Try a write query — should FAIL (read-only):
INSERT INTO users (name) VALUES ('test');
-- ERROR: cannot execute INSERT in a read-only transaction
Replication is working if:
pg_stat_replicationon primary showsstate = 'streaming'pg_stat_wal_receiveron replica showsstatus = 'streaming'- Read queries work on the replica
- Write queries are rejected on the replica
- Data written on the primary appears on the replica within seconds
11. Failover Procedures
If the primary database (Node 2) goes down, you can promote the replica to become the new primary. This is a manual process — use it as an emergency procedure.
Scenario: Primary Database Goes Down
Step 11a: Promote the Replica
PowerShell (Admin)# Promote the replica to primary
# Option 1: Using pg_ctl
& "C:\Program Files\PostgreSQL\18\bin\pg_ctl" promote -D "C:\Program Files\PostgreSQL\18\data"
# Option 2: Using SQL (connect to the replica)
psql -U postgres -c "SELECT pg_promote();"
# Verify it's no longer in recovery:
psql -U postgres -c "SELECT pg_is_in_recovery();"
# Expected: f (false) = it's now a primary!
Bash# Promote the replica
# Option 1: Using pg_ctl
sudo -u postgres pg_ctlcluster 15 main promote
# Option 2: Using SQL
sudo -u postgres psql -c "SELECT pg_promote();"
# Verify
sudo -u postgres psql -c "SELECT pg_is_in_recovery();"
# Expected: f (false)
Step 11b: Update Node 1's DATABASE_URL
On Node 1 (App Server), update the .env file to point to the promoted replica:
.env on Node 1# BEFORE (pointing to dead primary):
DATABASE_URL=postgresql://minusnow_user:YourStrongPassword123!@192.168.1.20:5432/minusnow
# AFTER (pointing to promoted replica):
DATABASE_URL=postgresql://minusnow_user:YourStrongPassword123!@192.168.1.30:5432/minusnow
Then restart the application:
Node 1# If running as a service:
# Windows: Restart-Service MinusNow
# Linux: sudo systemctl restart minusnow
# If running manually, stop and restart:
npm run start
Step 11c: Verify Application is Working with New Primary
From any machine# Health check
curl http://192.168.1.10:5000/api/health
# Should return "healthy" with the database now on 192.168.1.30
After failover, remember to:
- Investigate why the original primary went down
- Once fixed, you can reconfigure it as a new replica of the promoted primary
- Update your network planning table with the new primary IP
- Update firewall rules if needed
- Test the complete flow again
12. Monitoring Both Nodes
Install monitoring agents on both nodes to collect system metrics (CPU, memory, disk, uptime) and report them to the MinusNow application.
Step 12a: Install Agent on Node 1 (App Server)
PowerShell (Admin)# Download the agent installer from MinusNow
Invoke-WebRequest -Uri "http://192.168.1.10:5000/documentation/downloads/agent-install-windows.ps1" `
-OutFile agent-install-windows.ps1
# Run it
.\agent-install-windows.ps1 -Server "http://localhost:5000" -HostName "minusnow-app" -Interval 60
Bash# Download the agent
curl -o agent-install-linux.sh http://192.168.1.10:5000/documentation/downloads/agent-install-linux.sh
chmod +x agent-install-linux.sh
# Install
sudo ./agent-install-linux.sh --server "http://localhost:5000" --hostname "minusnow-app" --interval 60
Step 12b: Install Agent on Node 2 (DB Server)
PowerShell (Admin)# Download the agent from Node 1
Invoke-WebRequest -Uri "http://192.168.1.10:5000/documentation/downloads/agent-install-windows.ps1" `
-OutFile agent-install-windows.ps1
# Run it — NOTE: Server points to Node 1 (where the app runs)
.\agent-install-windows.ps1 -Server "http://192.168.1.10:5000" -HostName "minusnow-db" -Interval 60
Bash# Download the agent from Node 1
curl -o agent-install-linux.sh http://192.168.1.10:5000/documentation/downloads/agent-install-linux.sh
chmod +x agent-install-linux.sh
# Install — NOTE: Server points to Node 1
sudo ./agent-install-linux.sh --server "http://192.168.1.10:5000" --hostname "minusnow-db" --interval 60
Node 2's agent needs firewall access to Node 1's port 5000. If Node 2 cannot reach Node 1 on port 5000, add a firewall rule on Node 1:
# Windows (Node 1): New-NetFirewallRule -DisplayName "MinusNow HTTP" -Direction Inbound -Protocol TCP -LocalPort 5000 -Action Allow # Linux (Node 1): sudo ufw allow 5000/tcp comment "MinusNow App"
Step 12c: Monitor Replication Lag
If you have a replica, monitor the replication lag from the primary:
Node 2 (Primary) — psql# Check replication lag in bytes
SELECT
client_addr,
state,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag_bytes,
pg_size_pretty(pg_wal_lsn_diff(sent_lsn, replay_lsn)) AS lag_pretty
FROM pg_stat_replication;
-- Healthy output: lag_bytes close to 0, lag_pretty = "0 bytes" or "XX kB"
-- Warning: If lag_pretty shows MB or more, the replica is falling behind
You can create a cron job or scheduled task to alert on high lag:
Bash (Node 2)# Add to crontab: check replication lag every 5 minutes
*/5 * * * * sudo -u postgres psql -t -c "SELECT pg_wal_lsn_diff(sent_lsn, replay_lsn) FROM pg_stat_replication;" | awk '{if($1 > 104857600) print "ALERT: Replication lag > 100MB: " $1 " bytes"}' >> /var/log/minusnow-replication.log 2>&1
13. Backup Strategy (Two-Node)
In a two-node setup, you need to back up both nodes:
App Server Backup
Back up the application codebase, .env file, and any uploaded files:
Windows.\backup-windows.ps1 -OutputDir D:\backups
Linux./backup-linux.sh --output-dir /backups
Frequency: Weekly (code doesn't change often)
Database Server Backup
Back up the PostgreSQL database using pg_dump:
Windows.\backup-windows.ps1 -IncludeDB -PgDatabase minusnow
Linux./backup-linux.sh --include-db --pg-database minusnow
Frequency: Daily (data changes frequently)
Step 13a: Automated Database Backup Schedule
PowerShell (Admin)# Create a Scheduled Task for daily database backup at 2 AM
$action = New-ScheduledTaskAction `
-Execute "powershell.exe" `
-Argument "-File D:\MinusNow\documentation\downloads\backup-windows.ps1 -IncludeDB -PgDatabase minusnow -OutputDir D:\backups -RetainDays 30"
$trigger = New-ScheduledTaskTrigger -Daily -At "02:00AM"
Register-ScheduledTask -TaskName "MinusNow-DB-Backup" -Action $action -Trigger $trigger -RunLevel Highest
# Verify
Get-ScheduledTask -TaskName "MinusNow-DB-Backup"
Bash# Add to root's crontab: daily DB backup at 2 AM, retain 30 days
sudo crontab -e
# Add this line:
0 2 * * * /opt/minusnow/documentation/downloads/backup-linux.sh --include-db --pg-database minusnow --output-dir /backups --retain-days 30 >> /var/log/minusnow-backup.log 2>&1
Step 13b: Restore from Backup
If you need to restore the database on Node 2:
Node 2# Restore a SQL dump
# Windows:
psql -U postgres -d minusnow -f "D:\backups\minusnow-db-20260213-020000.sql"
# Linux:
sudo -u postgres psql -d minusnow -f /backups/minusnow-db-20260213-020000.sql
14. Security Hardening
14.1 Network Security
| Rule | Node 1 (App) | Node 2 (DB) |
|---|---|---|
| Port 5000 (HTTP) | Open to users/LAN | Closed |
| Port 5432 (PostgreSQL) | Closed | Open ONLY from Node 1 IP |
| Port 22 (SSH) | Admin only | Admin only |
| All other ports | Closed | Closed |
14.2 PostgreSQL Security
- Use
scram-sha-256authentication (notmd5ortrust) inpg_hba.conf - Never use the
postgressuperuser for application connections — useminusnow_user - Use
/32CIDR masks inpg_hba.conf— allow only exact IP addresses - Enable SSL/TLS for PostgreSQL connections (especially over WAN):
postgresql.confssl = on ssl_cert_file = '/path/to/server.crt' ssl_key_file = '/path/to/server.key'Then inpg_hba.conf: usehostsslinstead ofhost
14.3 Application Security
.envfile permissions:chmod 600 .envon Linux; on Windows, restrict ACL to the service user- SESSION_SECRET: Use a random 48+ character string (generated in Step 7b)
- HTTPS: Place a reverse proxy (Nginx, Caddy, or IIS) in front of Node 1 with SSL termination
- Rate limiting: Configure at the reverse proxy level
14.4 Backup Security
- Never include
.env,founder-credentials.json, oraudit-logs/in backups - Encrypt backup archives at rest (BitLocker on Windows, LUKS on Linux)
- Store backups on a separate volume or machine from the primary data
- Test restores regularly — a backup you can't restore is not a backup
15. Troubleshooting
15.1 Cannot Connect to Remote Database from Node 1
Symptom: ECONNREFUSED, Connection timed out, or no pg_hba.conf entry
Check these items in order:
- PostgreSQL is running on Node 2? Check
Get-Service postgresql*orsystemctl status postgresql listen_addressesinpostgresql.confis set to'*'or includes Node 2's IP?pg_hba.confhas ahostline for Node 1's IP?- Firewall on Node 2 allows port 5432 from Node 1?
- Network connectivity: Can you
pingNode 2 from Node 1? - Password correct? Test with
psql -h NODE2_IP -U minusnow_user -d minusnow
15.2 "permission denied for schema public"
Cause: PostgreSQL 15+ removed default CREATE privilege on the public schema.
Fix (run on Node 2)# Connect to the minusnow database as superuser
psql -U postgres -d minusnow
GRANT ALL ON SCHEMA public TO minusnow_user;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO minusnow_user;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO minusnow_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO minusnow_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO minusnow_user;
15.3 drizzle-kit "unknown command 'push'"
Cause: A global drizzle-kit (v0.18.x) overrides the local version (v0.31.x) needed by MinusNow.
Fix (run on Node 1)# Check for global version
npm list -g drizzle-kit
# If found, remove it
npm uninstall -g drizzle-kit
# Re-install local deps
cd D:\MinusNow # or /opt/minusnow
rm -rf node_modules package-lock.json
npm install
# Verify correct version
npx drizzle-kit --version # Should show 0.31.x
15.4 Replication Not Starting
Symptom: pg_stat_replication on primary shows no rows.
standby.signalexists in the replica's data directory?primary_conninfoinpostgresql.auto.confhas the correct primary IP, user, and password?- Firewall on primary allows port 5432 from the replica's IP?
pg_hba.confon primary has areplicationline for the replica's IP?wal_level = replicain primary'spostgresql.conf?- Check replica's PostgreSQL log for specific errors
15.5 High Replication Lag
Symptom: Data on replica is minutes or hours behind the primary.
- Network bandwidth between primary and replica may be saturated
- Disk I/O on replica — if the replica has slow disks, it can't apply WAL fast enough
- Increase
wal_keep_sizeon the primary to avoid WAL segments being recycled - Check
max_wal_sendershasn't been exceeded - Monitor with:
SELECT * FROM pg_stat_replication;on the primary
15.6 Application Shows "Connection Refused" After Failover
Cause: Node 1's .env still points to the old primary (Node 2) instead of the promoted replica.
Fix# Update .env on Node 1 to point to the new primary
# Change: @192.168.1.20 → @192.168.1.30
# Then restart the app:
# Windows: Restart-Service MinusNow
# Linux: sudo systemctl restart minusnow
15.7 psql Not Found on Windows
Cause: PostgreSQL installer doesn't add bin folder to PATH automatically.
Fix (PowerShell as Admin)# Add to system PATH (adjust version number)
$pgBin = "C:\Program Files\PostgreSQL\18\bin"
$current = [Environment]::GetEnvironmentVariable("Path","Machine")
[Environment]::SetEnvironmentVariable("Path","$current;$pgBin","Machine")
# Close and reopen PowerShell, then verify:
psql --version
15.8 Port 5000 Already in Use
Fix# Windows: Find and kill the process
Get-NetTCPConnection -LocalPort 5000 | Select-Object OwningProcess
Stop-Process -Id <PID> -Force
# Linux: Find and kill
sudo lsof -i :5000
sudo kill <PID>
15.9 Blank Page After Login
- Open browser DevTools (F12) → Console tab → check for JavaScript errors
- Build production assets:
npm run build - Check
NODE_ENV— in development, usenpm run devinstead ofnpm run start - Clear browser cache and try again
Appendix A: Quick Reference & Cheat Sheet
IP Address Reference (Replace with your actual IPs)
| Node | IP | Ports | Services |
|---|---|---|---|
| App Server (Node 1) | 192.168.1.10 | 5000 | Node.js, Express, MinusNow |
| DB Primary (Node 2) | 192.168.1.20 | 5432 | PostgreSQL (primary) |
| DB Replica (optional) | 192.168.1.30 | 5432 | PostgreSQL (standby) |
Key Files to Edit on Node 2 (DB Server)
| File | Purpose | Key Settings |
|---|---|---|
postgresql.conf | Server configuration | listen_addresses, wal_level, max_wal_senders |
pg_hba.conf | Client authentication | Allow Node 1's IP with scram-sha-256 |
Key File on Node 1 (App Server)
| File | Key Setting | Example |
|---|---|---|
.env | DATABASE_URL | postgresql://minusnow_user:Pass@192.168.1.20:5432/minusnow |
.env | SESSION_SECRET | 48+ character random string |
.env | PORT | 5000 |
.env | NODE_ENV | production |
Essential Commands Cheat Sheet
🖥️ Node 1 (App Server)
Commands# Install dependencies
npm install
# Push schema to remote DB
npm run db:push
# Build for production
npm run build
# Start production server
npm run start
# Check health
curl http://localhost:5000/api/health
🗄️ Node 2 (DB Server)
Commands# Check PostgreSQL status
# Win: Get-Service postgresql*
# Lin: systemctl status postgresql
# Restart PostgreSQL
# Win: Restart-Service postgresql*
# Lin: sudo systemctl restart postgresql
# Check active connections
psql -U postgres -c "SELECT client_addr, state FROM pg_stat_activity WHERE datname='minusnow';"
# Check replication (if replica)
psql -U postgres -c "SELECT client_addr, state FROM pg_stat_replication;"
# Backup database
pg_dump -U postgres minusnow > backup.sql
Deployment Checklist
- Node 2: PostgreSQL installed and running
- Node 2:
minusnowdatabase andminusnow_usercreated - Node 2: Schema permissions granted (PostgreSQL 15+)
- Node 2:
postgresql.conf—listen_addressesconfigured - Node 2:
pg_hba.conf— Node 1's IP allowed - Node 2: Firewall allows TCP 5432 from Node 1's IP only
- Node 2: PostgreSQL restarted after config changes
- Node 1: Node.js 20 LTS installed
- Node 1: MinusNow source code deployed
- Node 1:
npm installcompleted - Node 1:
.envcreated with remoteDATABASE_URL - Node 1: Database connection tested (
psqlor Node.js test) - Node 1:
npm run db:pushsucceeded - Node 1:
npm run buildcompleted - Node 1: Application started and accessible on port 5000
- Node 1: Health endpoint returns "healthy"
- Monitoring agents installed on both nodes
- Backup schedule configured on Node 2
- (Optional) Replica configured with streaming replication