Nginx proxy: local mode — Apache port migration, one-click enable/disable

- VhostManager: getApachePort() reads proxy_apache_port setting (default 80);
  writeApache() uses configured port; migrateApachePort() rewrites all vhosts
  and ports.conf; restoreApachePort() reverses the migration
- ProxyManager::switchToLocalMode() — generator: installs nginx if needed,
  migrates Apache to 8090, configs nginx catch-all, starts nginx, syncs proxy
  hosts; rolls back Apache on nginx config failure
- ProxyManager::disableLocalMode() — stops nginx, restores Apache to 80/443
- proxy.php: POST /api/proxy/switch-local and /api/proxy/disable-local (SSE stream)
- admin.js: two-card "not configured" layout (Local Mode / Remote VM);
  proxySwitchLocal() modal with port picker + live progress stream;
  proxyDisableLocal() reverts with progress; 'Disable Local Mode' in service
  controls when mode=local

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 10:30:33 +00:00
parent dc77c65a3f
commit c07639667b
4 changed files with 303 additions and 8 deletions
+33
View File
@@ -141,6 +141,39 @@ try {
$action === 'test-remote' && $method === 'POST' =>
Response::json(['success' => true, 'data' => ProxyManager::testRemote()]),
// POST switch-local — migrate Apache to internal port, install nginx, enable local proxy mode
($action === 'switch-local') && $method === 'POST' => (function() use ($body) {
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');
ob_implicit_flush(true);
while (ob_get_level() > 0) ob_end_flush();
$port = (int)($body['apache_port'] ?? 8090);
foreach (ProxyManager::switchToLocalMode($port) as $line) {
echo 'data: ' . json_encode(['line' => $line]) . "\n\n";
flush();
}
echo "data: " . json_encode(['done' => true]) . "\n\n";
flush();
exit;
})(),
// POST disable-local — revert: Apache back to 80, stop nginx, disable proxy
($action === 'disable-local') && $method === 'POST' => (function() {
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');
ob_implicit_flush(true);
while (ob_get_level() > 0) ob_end_flush();
foreach (ProxyManager::disableLocalMode() as $line) {
echo 'data: ' . json_encode(['line' => $line]) . "\n\n";
flush();
}
echo "data: " . json_encode(['done' => true]) . "\n\n";
flush();
exit;
})(),
// POST setup-remote — run nginx setup on remote VM, stream output via SSE
($action === 'setup-remote') && $method === 'POST' => (function() {
header('Content-Type: text/event-stream');
+109
View File
@@ -326,6 +326,115 @@ class ProxyManager {
return ['ok' => true, 'message' => 'Connected — ' . trim($out)];
}
// --- Local mode switch ---
/**
* Switch to local proxy mode:
* Apache moves to $apachePort (default 8090), nginx takes 80/443.
* All existing vhosts are re-written; proxy hosts synced automatically.
* Yields progress lines suitable for SSE streaming.
*/
public static function switchToLocalMode(int $apachePort = 8090): \Generator {
require_once NOVACPX_LIB . '/VhostManager.php';
$db = DB::getInstance();
$save = function(string $k, string $v) use ($db) {
$db->execute("INSERT INTO settings (`key`, value) VALUES (?,?) ON DUPLICATE KEY UPDATE value=VALUES(value)", [$k, $v]);
};
yield "» Checking nginx installation...\n";
if (!file_exists('/usr/sbin/nginx') && empty(shell_exec('which nginx 2>/dev/null'))) {
yield "» Installing nginx (apt-get install -y nginx)...\n";
$out = shell_exec('apt-get update -qq 2>&1 && apt-get install -y nginx 2>&1');
if ($out) yield trim($out) . "\n";
if (!file_exists('/usr/sbin/nginx')) { yield "ERROR: nginx install failed. Aborting.\n"; return; }
yield " nginx installed\n";
} else {
yield " nginx already installed\n";
}
yield "» Stopping nginx to avoid config conflicts...\n";
shell_exec('systemctl stop nginx 2>/dev/null');
yield "» Migrating Apache from port 80 → {$apachePort}...\n";
$changed = VhostManager::migrateApachePort(80, $apachePort);
yield " Updated {$changed} vhost(s) and ports.conf\n";
yield "» Restarting Apache on port {$apachePort}...\n";
$apacheTest = shell_exec('apache2ctl configtest 2>&1');
if (strpos($apacheTest ?? '', 'Syntax OK') === false) {
yield "ERROR: Apache config test failed:\n{$apacheTest}\nRolling back...\n";
VhostManager::restoreApachePort($apachePort, 80);
shell_exec('systemctl restart apache2 2>/dev/null');
yield " Apache restored to port 80. Aborting.\n";
return;
}
shell_exec('systemctl restart apache2 2>/dev/null');
yield " Apache is up on port {$apachePort}\n";
yield "» Configuring nginx (remove default site, add catch-all)...\n";
@unlink('/etc/nginx/sites-enabled/default');
$catchAll = "server {\n listen 80 default_server;\n server_name _;\n return 444;\n}\n";
file_put_contents('/etc/nginx/sites-available/novacpx-default.conf', $catchAll);
if (!file_exists('/etc/nginx/sites-enabled/novacpx-default.conf')) {
@symlink('/etc/nginx/sites-available/novacpx-default.conf', '/etc/nginx/sites-enabled/novacpx-default.conf');
}
if (!file_exists('/etc/nginx/conf.d/novacpx-proxy.conf')) {
file_put_contents('/etc/nginx/conf.d/novacpx-proxy.conf',
"client_max_body_size 256M;\nproxy_buffers 16 16k;\nproxy_buffer_size 16k;\n");
}
yield "» Saving proxy settings...\n";
$save('proxy_mode', 'local');
$save('proxy_backend_ip', '127.0.0.1');
$save('proxy_apache_port', (string)$apachePort);
yield "» Starting nginx on port 80/443...\n";
shell_exec('systemctl enable nginx 2>/dev/null && systemctl start nginx 2>/dev/null');
sleep(1);
if (!self::isRunning()) {
$err = shell_exec('nginx -t 2>&1');
yield "ERROR: nginx failed to start:\n{$err}\n";
yield "Apache is running on port {$apachePort}. Fix nginx config and try again.\n";
return;
}
yield " nginx is running\n";
yield "» Syncing proxy hosts from all active accounts...\n";
$added = self::syncFromAccounts();
yield " Added {$added} proxy host(s)\n";
self::writeAllConfigs();
yield "✓ Local proxy mode active!\n";
yield " Apache: 127.0.0.1:{$apachePort} (PHP, file serving)\n";
yield " Nginx: 0.0.0.0:80/443 (public, proxies to Apache)\n";
yield " All accounts route through nginx → Apache automatically.\n";
}
/**
* Revert local mode: move Apache back to 80/443, stop nginx, disable proxy.
*/
public static function disableLocalMode(): \Generator {
require_once NOVACPX_LIB . '/VhostManager.php';
$db = DB::getInstance();
$apachePort = (int)($db->fetchOne("SELECT value FROM settings WHERE `key`='proxy_apache_port'")['value'] ?? 8090);
yield "» Stopping nginx...\n";
shell_exec('systemctl stop nginx 2>/dev/null && systemctl disable nginx 2>/dev/null');
yield "» Migrating Apache from port {$apachePort} → 80...\n";
$changed = VhostManager::restoreApachePort($apachePort);
yield " Updated {$changed} vhost(s) and ports.conf\n";
yield "» Restarting Apache on port 80...\n";
shell_exec('systemctl restart apache2 2>/dev/null');
yield "» Saving settings...\n";
$db->execute("INSERT INTO settings (`key`, value) VALUES ('proxy_mode','disabled') ON DUPLICATE KEY UPDATE value='disabled'");
$db->execute("UPDATE settings SET value='80' WHERE `key`='proxy_apache_port'");
yield "✓ Proxy disabled. Apache is back on port 80/443.\n";
}
// --- Remote setup & uninstall ---
public static function runSetupOnRemote(): \Generator {
+52 -1
View File
@@ -117,10 +117,61 @@ class VhostManager {
@symlink($conf, "/etc/nginx/sites-enabled/novacpx-{$username}.conf");
}
// Returns the port Apache listens on for customer vhosts.
// 80 normally; changes to an internal port (e.g. 8090) in local proxy mode.
public static function getApachePort(): int {
$db = DB::getInstance();
return (int)($db->fetchOne("SELECT value FROM settings WHERE `key`='proxy_apache_port'")['value'] ?? 80);
}
// Re-write all existing novacpx-*.conf vhosts to use $to instead of $from,
// and update /etc/apache2/ports.conf accordingly. Returns count of files changed.
public static function migrateApachePort(int $from, int $to): int {
$count = 0;
foreach (glob('/etc/apache2/sites-available/novacpx-*.conf') ?: [] as $f) {
$orig = file_get_contents($f);
$updated = str_replace(
["<VirtualHost *:{$from}>", "VirtualHost *:{$from}"],
["<VirtualHost *:{$to}>", "VirtualHost *:{$to}"],
$orig
);
if ($updated !== $orig) { file_put_contents($f, $updated); $count++; }
}
// Update ports.conf: swap Listen $from → Listen $to, drop Listen 443 (nginx handles SSL)
$ports = file_get_contents('/etc/apache2/ports.conf') ?: '';
$ports = preg_replace('/^Listen\s+' . $from . '\b/m', "Listen {$to}", $ports);
$ports = preg_replace('/^Listen\s+443\b/m', '', $ports);
$ports = preg_replace('/<IfModule ssl_module>.*?<\/IfModule>/s', '', $ports);
file_put_contents('/etc/apache2/ports.conf', $ports);
return $count;
}
// Reverse migration: move Apache back from proxy port to standard 80/443.
public static function restoreApachePort(int $from, int $to = 80): int {
$count = 0;
foreach (glob('/etc/apache2/sites-available/novacpx-*.conf') ?: [] as $f) {
$orig = file_get_contents($f);
$updated = str_replace(
["<VirtualHost *:{$from}>", "VirtualHost *:{$from}"],
["<VirtualHost *:{$to}>", "VirtualHost *:{$to}"],
$orig
);
if ($updated !== $orig) { file_put_contents($f, $updated); $count++; }
}
$ports = file_get_contents('/etc/apache2/ports.conf') ?: '';
$ports = preg_replace('/^Listen\s+' . $from . '\b/m', "Listen {$to}", $ports);
if (!str_contains($ports, 'Listen 443')) {
$ports .= "\n<IfModule ssl_module>\n Listen 443\n</IfModule>\n";
}
file_put_contents('/etc/apache2/ports.conf', $ports);
return $count;
}
private static function writeApache(string $username, string $domain, string $docRoot, string $phpVer, string $logDir): void {
$port = self::getApachePort();
$sock = "/run/php/php{$phpVer}-fpm-{$username}.sock";
$conf = "/etc/apache2/sites-available/novacpx-{$username}.conf";
file_put_contents($conf, "<VirtualHost *:80>
file_put_contents($conf, "<VirtualHost *:{$port}>
ServerName {$domain}
ServerAlias www.{$domain}
DocumentRoot {$docRoot}
+109 -7
View File
@@ -2651,13 +2651,23 @@ async function nginxProxyPage() {
</div>
${!inst ? `
<div class="panel" style="text-align:center;padding:3rem">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="48" height="48" style="color:var(--text-muted);margin-bottom:1rem"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
<h3 style="margin-bottom:0.5rem">Nginx Proxy Not Active</h3>
<p style="color:var(--text-muted);margin-bottom:1.5rem">Use a dedicated proxy VM (recommended) run nginx on a separate LXC and control it from here via SSH. Or install nginx locally alongside Apache (requires moving Apache to port 8080).</p>
<div style="display:flex;gap:0.75rem;justify-content:center;flex-wrap:wrap">
<button class="btn btn-primary" onclick="proxySettings()">Configure Remote Proxy VM</button>
<button class="btn btn-secondary" onclick="proxySetupInstructions()">Setup Guide</button>
<div class="panel" style="padding:2rem">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;max-width:680px;margin:0 auto">
<div style="border:1px solid var(--border);border-radius:8px;padding:1.5rem;text-align:center">
<div style="font-size:2rem;margin-bottom:0.5rem">🖥</div>
<h4 style="margin-bottom:0.5rem">Local Mode</h4>
<p style="color:var(--text-muted);font-size:0.85rem;margin-bottom:1rem">nginx on <em>this server</em>. Apache moves to an internal port. All websites keep working nginx proxies everything through. One-click setup.</p>
<button class="btn btn-primary btn-sm" onclick="proxySwitchLocal()">Enable Local Mode</button>
</div>
<div style="border:1px solid var(--border);border-radius:8px;padding:1.5rem;text-align:center">
<div style="font-size:2rem;margin-bottom:0.5rem">🌐</div>
<h4 style="margin-bottom:0.5rem">Remote Proxy VM</h4>
<p style="color:var(--text-muted);font-size:0.85rem;margin-bottom:1rem">Dedicated LXC or VM runs nginx. Panel pushes configs via SSH. Best for production keeps proxy and hosting isolated.</p>
<button class="btn btn-secondary btn-sm" onclick="proxySettings()">Configure Remote VM</button>
</div>
</div>
<div style="text-align:center;margin-top:1.25rem">
<button class="btn btn-ghost btn-sm" onclick="proxySetupInstructions()">Setup Guide &amp; Requirements</button>
</div>
</div>
` : `
@@ -2669,6 +2679,7 @@ ${!inst ? `
<button class="btn btn-sm btn-warning" onclick="proxyControl('restart')">Restart</button>
<button class="btn btn-sm btn-danger" onclick="proxyControl('stop')">Stop</button>
<button class="btn btn-sm btn-ghost" onclick="proxyControl('reload')">Reload Config</button>
${cfg.mode === 'local' ? `<button class="btn btn-sm btn-ghost" style="margin-left:0.5rem" onclick="proxyDisableLocal()">Disable Local Mode</button>` : ''}
</div>
</div>
</div>
@@ -2876,6 +2887,97 @@ window.proxySetupInstructions = async () => {
`, null, { cancelLabel: 'Close', showConfirm: false });
};
window.proxySwitchLocal = () => {
Nova.modal('Enable Local Nginx Proxy', `
<p style="margin-bottom:1rem">Nginx will be installed on <em>this server</em> and take over ports 80/443. Apache moves to an internal port and keeps serving all PHP sites end users see no change.</p>
<div style="background:var(--bg-secondary);padding:0.75rem;border-radius:6px;font-size:0.85rem;margin-bottom:1rem">
<strong>What will happen:</strong><br>
<span style="color:var(--text-muted)">
1. nginx installed (if not present)<br>
2. Apache moved from port 80 <strong id="sl-port-preview">8090</strong><br>
3. All existing vhosts updated<br>
4. nginx starts on port 80/443 and proxies to Apache<br>
5. Proxy hosts auto-synced from your accounts
</span>
</div>
<div class="form-group">
<label>Apache backend port <small class="text-muted">(any unused port; 8090 is the default)</small></label>
<input id="sl-port" type="number" class="form-control" value="8090" min="1024" max="65535"
oninput="document.getElementById('sl-port-preview').textContent=this.value">
</div>
`, () => {
const port = parseInt(document.getElementById('sl-port')?.value) || 8090;
const ov = Nova.modal('Switching to Local Proxy Mode', `
<p style="color:var(--text-muted);margin-bottom:0.75rem">Moving Apache to port ${port} and starting nginx on 80/443</p>
<pre id="proxy-local-log" style="background:var(--bg-secondary);padding:0.75rem;border-radius:6px;font-size:0.78rem;max-height:320px;overflow-y:auto;white-space:pre-wrap;font-family:monospace">Starting\n</pre>
`, null, { cancelLabel: 'Close', showConfirm: false });
const log = document.getElementById('proxy-local-log');
const es = new EventSource('/api/proxy/switch-local');
let done = false;
// POST with port — can't use native EventSource for POST, so use fetch+ReadableStream
es.close();
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; });
}, { confirmLabel: 'Switch Now' });
};
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', `
<pre id="proxy-disable-log" style="background:var(--bg-secondary);padding:0.75rem;border-radius:6px;font-size:0.78rem;max-height:240px;overflow-y:auto;white-space:pre-wrap;font-family:monospace">Starting\n</pre>
`, 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', `
<p style="color:var(--text-muted);margin-bottom:0.75rem">Running setup on the remote proxy VM this takes about 30 seconds.</p>