Install Nginx alongside Apache on this VM. Nginx listens on ports 80/443 and forwards to Apache. Best for SSL termination and caching.
-
Option B — Remote Proxy VM (Recommended for production)
-
Run a dedicated Nginx proxy VM in front of this NovaCPX VM. Traffic flows: Internet → FortiGate → Nginx Proxy VM → NovaCPX VM (Apache).
-
- - Create a new VM on Proxmox (Ubuntu 22.04, 1 vCPU, 1GB RAM)
- - Run the setup script below on the new VM as root
- - Point FortiGate VIPs to the proxy VM IP (ports 80/443)
- - Set the proxy upstream to this NovaCPX VM IP (
http://10.48.200.110:80)
- - Add proxy hosts for each domain from your NovaCPX admin panel
-
-
-
Automated Setup Script
-
Run this on the target VM (local or remote) as root:
-
- curl -sk https://YOUR_NOVACPX_IP:8882/api/proxy/setup-script | bash
-
-
Or download and review before running:
-
- curl -sk https://YOUR_NOVACPX_IP:8882/api/proxy/setup-script -o proxy-setup.sh
- cat proxy-setup.sh # review
- bash proxy-setup.sh
+
+ Designed for Proxmox (or any Linux hypervisor)
+
+ Run NovaCPX on one VM and a lightweight Debian LXC as the nginx proxy.
+ The panel pushes configs and controls nginx via SSH.
+ Works equally well on VMware, AWS, DigitalOcean, bare-metal — see Option C below.
+
-
Integration with VirtualHost Manager
-
When proxy mode is active, NovaCPX automatically:
-
- - Creates a proxy host entry for every new account
- - Removes the proxy host when an account is terminated
- - Re-generates Nginx config on every account change
- - Uses account SSL certs automatically if SSL is enabled on the proxy host
+ Option A — Proxmox LXC (Recommended)
+ Create a 512MB Debian 12 LXC on the same Proxmox node. Costs almost no resources.
+
+ - In Proxmox: Create CT → Debian 12 → 512MB RAM, 8GB disk, same bridge as NovaCPX VM
+ - Boot the LXC, set root password
+ - Go to Settings → set Mode=Remote, enter the LXC IP, root password, and this VM's IP as Backend IP
+ - Click Run Setup on Remote VM — watch live progress
+ - Point your router/firewall port 80/443 to the LXC IP
+ - Click Sync Accounts to auto-populate proxy hosts
+
+
+ Option B — Other hypervisors (VMware, Hyper-V, KVM)
+ Same flow — any Debian/Ubuntu VM reachable by SSH works.
+
+ - Create a Debian/Ubuntu VM (1 vCPU, 512MB RAM)
+ - Enable SSH root login:
PermitRootLogin yes in /etc/ssh/sshd_config
+ - Install
sshpass on the NovaCPX server: apt-get install -y sshpass
+ - Follow steps 3–6 from Option A above
+
+
+ Option C — Cloud / Remote Server (AWS, DigitalOcean, etc.)
+ NovaCPX pushes configs via public SSH. The proxy VM's public IP handles port 80/443; it forwards to NovaCPX over a private network or VPN.
+
+ - Provision a small Debian droplet/instance in the same region or with low latency to NovaCPX
+ - Open port 22 (SSH) from NovaCPX's IP only; open 80/443 from anywhere
+ - Set Backend IP to NovaCPX's IP reachable from the cloud proxy (use VPN/private IP if possible)
+ - In Settings: set Remote Host to the cloud server's public IP or hostname
+ - Click Run Setup, then Sync Accounts
+
+
+ Option D — Local nginx on this VM
+ Not recommended — requires moving Apache off port 80/443 first.
+
+ - Edit
/etc/apache2/ports.conf → change Listen 80 to Listen 8090, restart Apache
+ - Set Settings → Mode = Local, Backend IP = 127.0.0.1
+ - Click Install Nginx Locally
+ - Set upstream
http://127.0.0.1:8090 on all proxy hosts
+ - Click Sync Accounts
+
+
+ Settings Reference (Admin → Nginx Proxy → Settings)
+
+ | Field | Description |
+ Mode | disabled / remote / local |
+ Remote Host | IP or hostname of nginx proxy VM (SSH target) |
+ Remote User | SSH user on proxy VM (default: root) |
+ Remote Password | SSH password (stored encrypted in DB) |
+ Backend IP | IP of this NovaCPX Apache — used in auto-generated proxy upstream URLs |
+
+
+ How it works
+
+ - Each domain gets an nginx vhost config on the proxy VM, proxying to Apache on the backend IP
+ - Configs are pushed automatically when accounts are created/terminated or manually via Sync Accounts
+ - The panel starts/stops/reloads nginx on the proxy VM over SSH
+ - Every 5 minutes the health check verifies nginx is running and restarts it if not
+ - Use Uninstall to remove proxy configs or wipe nginx from the remote VM entirely
`, null, { cancelLabel: 'Close', showConfirm: false });
};
+window.proxySwitchLocal = () => {
+ const slOv = Nova.modal('Enable Local Nginx Proxy', `
+
Nginx will be installed on this server and take over ports 80/443. Apache moves to an internal port and keeps serving all PHP sites — end users see no change.
+
+ What will happen:
+
+ 1. nginx installed (if not present)
+ 2. Apache moved from port 80 → 8090
+ 3. All existing vhosts updated
+ 4. nginx starts on port 80/443 and proxies to Apache
+ 5. Proxy hosts auto-synced from your accounts
+
+
+
+
+
+
+ `,
+ `
+
`
+ );
+ slOv.querySelector('#sl-switch-btn').addEventListener('click', () => {
+ slOv.remove();
+ const port = parseInt(document.getElementById('sl-port')?.value) || 8090;
+ const ov = Nova.modal('Switching to Local Proxy Mode', `
+
Moving Apache to port ${port} and starting nginx on 80/443…
+
Starting…\n
+ `, null, { cancelLabel: 'Close', showConfirm: false });
+
+ const log = document.getElementById('proxy-local-log');
+ let done = false;
+
+ fetch('/api/proxy/switch-local', {
+ method: 'POST',
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ apache_port: port }),
+ }).then(async res => {
+ const reader = res.body.getReader();
+ const dec = new TextDecoder();
+ let buf = '';
+ while (true) {
+ const { value, done: d } = await reader.read();
+ if (d) break;
+ buf += dec.decode(value, { stream: true });
+ const parts = buf.split('\n\n');
+ buf = parts.pop();
+ for (const part of parts) {
+ const m = part.match(/^data: (.+)$/m);
+ if (!m) continue;
+ try {
+ const evt = JSON.parse(m[1]);
+ if (evt.line) { log.textContent += evt.line; log.scrollTop = log.scrollHeight; }
+ if (evt.done) { done = true; log.textContent += '\n— Done.\n'; setTimeout(() => Nova.loadPage('nginx-proxy', window._novaPages), 1500); }
+ } catch {}
+ }
+ }
+ }).catch(e => { log.textContent += '\n— Connection error: ' + e.message + '\n'; });
+
+ ov.querySelector('.modal-close')?.addEventListener('click', () => { done = true; });
+ });
+};
+
+window.proxyDisableLocal = () => {
+ Nova.confirm('Revert to direct Apache mode? nginx will be stopped and Apache will move back to port 80.', () => {
+ const ov = Nova.modal('Disabling Local Proxy Mode', `
+
Starting…\n
+ `, null, { cancelLabel: 'Close', showConfirm: false });
+ const log = document.getElementById('proxy-disable-log');
+ fetch('/api/proxy/disable-local', { method: 'POST', credentials: 'include' }).then(async res => {
+ const reader = res.body.getReader();
+ const dec = new TextDecoder();
+ let buf = '';
+ while (true) {
+ const { value, done } = await reader.read();
+ if (done) break;
+ buf += dec.decode(value, { stream: true });
+ const parts = buf.split('\n\n');
+ buf = parts.pop();
+ for (const part of parts) {
+ const m = part.match(/^data: (.+)$/m);
+ if (m) try {
+ const evt = JSON.parse(m[1]);
+ if (evt.line) { log.textContent += evt.line; log.scrollTop = log.scrollHeight; }
+ if (evt.done) setTimeout(() => Nova.loadPage('nginx-proxy', window._novaPages), 1000);
+ } catch {}
+ }
+ }
+ });
+ }, true);
+};
+
+window.proxyRunSetup = () => {
+ const ov = Nova.modal('Setting Up Remote Nginx Proxy', `
+
Running setup on the remote proxy VM — this takes about 30 seconds.
+
Connecting…\n
+ `);
+
+ const log = document.getElementById('proxy-setup-log');
+ let done = false;
+
+ fetch('/api/proxy/setup-remote', { method: 'POST', credentials: 'include' })
+ .then(async res => {
+ if (!res.ok) { log.textContent += '\n— Server error (' + res.status + '). Check remote host settings.\n'; return; }
+ const reader = res.body.getReader();
+ const dec = new TextDecoder();
+ let buf = '';
+ while (true) {
+ const { value, done: d } = await reader.read();
+ if (d) break;
+ buf += dec.decode(value, { stream: true });
+ const parts = buf.split('\n\n');
+ buf = parts.pop();
+ for (const part of parts) {
+ const m = part.match(/^data: (.+)$/m);
+ if (!m) continue;
+ try {
+ const evt = JSON.parse(m[1]);
+ if (evt.line) { log.textContent += evt.line; log.scrollTop = log.scrollHeight; }
+ if (evt.done) { done = true; log.textContent += '\n— Done. Refreshing status…\n'; setTimeout(() => Nova.loadPage('nginx-proxy', window._novaPages), 1200); }
+ } catch {}
+ }
+ }
+ })
+ .catch(e => { log.textContent += '\n— Connection error: ' + e.message + '\n'; });
+
+ ov.querySelector('.modal-close')?.addEventListener('click', () => { done = true; });
+};
+
+window.proxyUninstall = () => {
+ const ov = Nova.modal('Uninstall Nginx Proxy', `
+
Choose what to remove from the remote proxy VM:
+
+
+
+
+ `,
+ `
+
`
+ );
+ ov.querySelector('#uninst-btn').addEventListener('click', async () => {
+ const btn = ov.querySelector('#uninst-btn');
+ btn.disabled = true; btn.textContent = 'Removing…';
+ const full = ov.querySelector('input[name="uninst"]:checked')?.value === 'full';
+ const r = await Nova.api('proxy', 'uninstall', { method: 'DELETE', body: { remove_nginx: full } });
+ Nova.toast(r?.data?.result || r?.message || 'Done', r?.success ? 'success' : 'error');
+ if (r?.success) { ov.remove(); Nova.loadPage('nginx-proxy', window._novaPages); }
+ else { btn.disabled = false; btn.textContent = 'Uninstall'; }
+ });
+};
+
+window.proxySettings = async () => {
+ const r = await Nova.api('proxy', 'settings');
+ const cfg = r?.data || {};
+ const ov = Nova.modal('Nginx Proxy Settings', `
+
+
+
+
+
+ `,
+ `
+
`
+ );
+ ov.querySelector('#ps-save-btn').addEventListener('click', async () => {
+ const btn = ov.querySelector('#ps-save-btn');
+ btn.disabled = true; btn.textContent = 'Saving…';
+ const mode = document.getElementById('ps-mode')?.value;
+ const pass = document.getElementById('ps-pass')?.value;
+ const body = {
+ mode,
+ remote_host: document.getElementById('ps-host')?.value?.trim() || '',
+ remote_user: document.getElementById('ps-user')?.value?.trim() || 'root',
+ remote_pass: pass || '••••••••',
+ backend_ip: document.getElementById('ps-backend')?.value?.trim() || '',
+ };
+ const r = await Nova.api('proxy', 'settings', { method: 'POST', body });
+ Nova.toast(r?.success ? 'Settings saved' : (r?.message || 'Failed'), r?.success ? 'success' : 'error');
+ if (r?.success) { ov.remove(); Nova.loadPage('nginx-proxy', window._novaPages); }
+ else { btn.disabled = false; btn.textContent = 'Save Settings'; }
+ });
+};
+
+window.proxyTestRemote = async () => {
+ const host = document.getElementById('ps-host')?.value?.trim();
+ const user = document.getElementById('ps-user')?.value?.trim() || 'root';
+ const pass = document.getElementById('ps-pass')?.value;
+ const el = document.getElementById('ps-test-result');
+ if (!host) { if (el) el.textContent = 'Enter a host first'; return; }
+ if (el) el.textContent = 'Testing…';
+ // Save current fields temporarily so the test can use them
+ await Nova.api('proxy', 'settings', { method: 'POST', body: {
+ remote_host: host, remote_user: user,
+ remote_pass: pass || '••••••••',
+ }});
+ const r = await Nova.api('proxy', 'test-remote', { method: 'POST' });
+ const d = r?.data || {};
+ if (el) {
+ el.style.color = d.ok ? 'var(--color-success)' : 'var(--color-error)';
+ el.textContent = d.message || (d.ok ? 'Connected' : 'Failed');
+ }
+};
+
// ── #29 Session Manager ───────────────────────────────────────────────────────
async function sessionsPage() {
const r = await Nova.api('sessions', 'list');
@@ -2906,8 +4092,8 @@ async function docker() {
${(status.disk||[]).map(d=>`
${Nova.escHtml(d.Type||d.type||'?')}
${Nova.escHtml(d.TotalCount||d.Size||'—')}
${Nova.escHtml(d.Reclaimable||d.reclaimable||'')}
`).join('')}
- ${tab('containers','Containers')} ${tab('images','Images')} ${tab('volumes','Volumes')} ${tab('networks','Networks')} ${tab('stacks','Compose Stacks')} ${tab('quotas','User Quotas')}
-
+ ${tab('containers','Containers')} ${tab('images','Images')} ${tab('volumes','Volumes')} ${tab('networks','Networks')} ${tab('stacks','Compose Stacks')} ${tab('catalog','App Catalog')} ${tab('quotas','User Quotas')}
+