Introduction
I wanted a simple landing page for my Proxmox servers. Instead of remembering the addresses and ports for each server and service, I wanted to enter a hostname in my browser and get a page with links to everything I use regularly.
The solution is a basic HTML dashboard served directly from the Proxmox host. There is no need to install another web server or run an additional container. We will use Python to serve the page and systemd to keep the service running.
The dashboard will listen on port 80, leaving the Proxmox management interface on port 8006. We will also configure DNS so the dashboard can be accessed using a hostname.
1. Create the Dashboard Directory
SSH into your Proxmox host with an account that has root privileges.
Create a directory for the dashboard.
mkdir -p /var/www2. Create the Dashboard HTML File
Create the HTML file.
nano -w /var/www/host-dashboard.htmlPaste the following HTML into the file.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Local AI Server Dashboard</title>
<style>
:root {
color-scheme: dark;
--background: #111318;
--card-background: #1b1e25;
--card-border: #303541;
--text: #f1f3f5;
--muted: #a0a7b4;
--accent: #e04444;
--accent-hover: #ff5b5b;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: var(--background);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Helvetica, Arial, sans-serif;
}
header {
padding: 3rem 1.5rem 2rem;
text-align: center;
border-bottom: 1px solid var(--card-border);
}
header h1 {
margin: 0;
font-size: clamp(2rem, 5vw, 3rem);
font-weight: 700;
letter-spacing: -0.04em;
}
header p {
margin: 0.8rem 0 0;
color: var(--muted);
font-size: 1rem;
}
main {
width: min(1100px, 100%);
margin: 0 auto;
padding: 2.5rem 1.5rem;
}
.section-heading {
margin: 0 0 1.5rem;
font-size: 1.2rem;
font-weight: 600;
}
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.25rem;
}
.service-card {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1.5rem;
background: var(--card-background);
border: 1px solid var(--card-border);
border-radius: 12px;
text-decoration: none;
color: var(--text);
transition: border-color 0.2s ease,
transform 0.2s ease,
background 0.2s ease;
}
.service-card:hover {
border-color: var(--accent);
background: #20242d;
transform: translateY(-3px);
}
.service-card:focus-visible {
outline: 2px solid var(--accent-hover);
outline-offset: 3px;
}
.service-icon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 48px;
height: 48px;
border-radius: 10px;
background: #302126;
color: var(--accent-hover);
}
.service-icon svg {
width: 26px;
height: 26px;
}
.service-info {
min-width: 0;
}
.service-info h3 {
margin: 0 0 0.45rem;
font-size: 1.05rem;
font-weight: 650;
}
.service-info p {
margin: 0;
color: var(--muted);
font-size: 0.9rem;
line-height: 1.5;
overflow-wrap: anywhere;
}
footer {
padding: 1.5rem;
text-align: center;
color: var(--muted);
font-size: 0.8rem;
border-top: 1px solid var(--card-border);
}
@media (max-width: 640px) {
header {
padding: 2rem 1rem 1.5rem;
}
main {
padding: 1.5rem 1rem;
}
.services-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<header>
<h1>Local AI Server</h1>
<p>Infrastructure and service dashboard</p>
</header>
<main>
<h2 class="section-heading">Services</h2>
<div id="services-grid" class="services-grid"></div>
</main>
<footer>
Local Infrastructure Dashboard
</footer>
<script>
const services = [
{
name: "Proxmox Host",
description: "Virtual machines, containers, and host management",
url: "https://192.168.1.1:8006",
icon: "server"
},
{
name: "Dockge UI",
description: "Manage Docker Compose applications",
url: "http://192.168.1.2:5001",
icon: "cpu"
}
];
const icons = {
server: `
<svg viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.8"
stroke-linecap="round" stroke-linejoin="round"
aria-hidden="true">
<rect x="3" y="3" width="18" height="7" rx="2"></rect>
<rect x="3" y="14" width="18" height="7" rx="2"></rect>
<path d="M7 6.5h.01M7 17.5h.01M11 6.5h6M11 17.5h6"></path>
</svg>
`,
cpu: `
<svg viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.8"
stroke-linecap="round" stroke-linejoin="round"
aria-hidden="true">
<rect x="6" y="6" width="12" height="12" rx="2"></rect>
<rect x="9" y="9" width="6" height="6" rx="1"></rect>
<path d="M9 2v4M15 2v4M9 18v4M15 18v4M2 9h4M2 15h4M18 9h4M18 15h4"></path>
</svg>
`,
network: `
<svg viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.8"
stroke-linecap="round" stroke-linejoin="round"
aria-hidden="true">
<rect x="9" y="2" width="6" height="6" rx="1"></rect>
<rect x="2" y="16" width="6" height="6" rx="1"></rect>
<rect x="16" y="16" width="6" height="6" rx="1"></rect>
<path d="M12 8v4M5 16v-4h14v4"></path>
</svg>
`
};
const grid = document.getElementById("services-grid");
services.forEach((service) => {
const card = document.createElement("a");
card.className = "service-card";
card.href = service.url;
card.target = "_blank";
card.rel = "noopener noreferrer";
card.setAttribute("aria-label", service.name);
const icon = document.createElement("div");
icon.className = "service-icon";
icon.setAttribute("aria-hidden", "true");
icon.innerHTML = icons[service.icon] || icons.server;
const info = document.createElement("div");
info.className = "service-info";
const title = document.createElement("h3");
title.textContent = service.name;
const description = document.createElement("p");
description.textContent = service.description;
info.appendChild(title);
info.appendChild(description);
card.appendChild(icon);
card.appendChild(info);
grid.appendChild(card);
});
</script>
</body>
</html>Customize the Dashboard
The example includes three service links. Change the names, descriptions, and URLs in the services array to match your own environment.
For example:
{
name: "Proxmox Host",
description: "Virtual machines and containers",
url: "https://192.168.1.10:8006",
icon: "server"
}The available icon types in this example are server, cpu, and network. You can reuse them for other services.
The dashboard is a static HTML file. You do not need to rebuild or restart the web server when you edit it.
3. Create the Python Web Server
We will use Python’s built-in HTTP server functionality to serve the dashboard. The script returns the same HTML file regardless of the URL path entered in the browser.
Create the Python script.
nano -w /usr/local/sbin/dashboard-server.pyPaste the following code.
#!/usr/bin/env python3
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
HOST = "192.168.1.1"
PORT = 80
HTML_FILE = Path("/var/www/host-dashboard.html")
class DashboardHandler(BaseHTTPRequestHandler):
def do_GET(self):
try:
content = HTML_FILE.read_bytes()
except OSError:
self.send_error(500, "Unable to read dashboard file")
return
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(content)))
self.send_header("Cache-Control", "no-cache")
self.end_headers()
self.wfile.write(content)
def log_message(self, format, *args):
print("%s - %s" % (
self.address_string(),
format % args
))
if __name__ == "__main__":
server = HTTPServer((HOST, PORT), DashboardHandler)
print(f"Dashboard server listening on {HOST}:{PORT}")
server.serve_forever()Important: Change 192.168.1.1 to the IP address of your Proxmox host. This must be the address on which you want the dashboard to listen.
Save the file and make it executable.
chmod 755 /usr/local/sbin/dashboard-server.pyThe server listens only on the configured IP address and port. It does not change the Proxmox management interface on port 8006.
4. Create a Systemd Service
We want the dashboard to start automatically when Proxmox boots and restart if the Python process exits.
Create a systemd service file.
nano -w /etc/systemd/system/dashboard-server.serviceAdd the following configuration.
[Unit]
Description=Proxmox Host Dashboard
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
ExecStart=/usr/bin/python3 /usr/local/sbin/dashboard-server.py
Restart=always
RestartSec=5
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
[Install]
WantedBy=multi-user.targetSave the file.
The service runs as www-data instead of root. The CAP_NET_BIND_SERVICE capability allows the process to listen on port 80 without running the service as root.
5. Start the Dashboard
Before starting the service, check whether another service is already listening on port 80.
ss -ltnp 'sport = :80'If another service is using port 80 on the Proxmox host, resolve that conflict before proceeding. A service listening on port 80 inside a separate container does not necessarily conflict with this host service; the important thing is whether the host itself has port 80 occupied.
Reload systemd and start the dashboard.
systemctl daemon-reload
systemctl enable --now dashboard-server.serviceCheck the service status.
systemctl status dashboard-server.serviceTest the dashboard from the Proxmox host, replacing the example IP address with your own.
curl -I http://192.168.1.1/You should receive an HTTP response with status 200 OK.
You can also test the HTML content directly:
curl http://192.168.1.1/If the service fails to start, check its logs.
journalctl -u dashboard-server.service -n 50 --no-pager6. Configure DNS
The final step is to configure a DNS record for the dashboard.
In your DNS server, create an A record pointing your chosen hostname to the Proxmox host’s IP address.
For example:
vmhost1.example.com A 192.168.1.1Replace the example hostname and IP address with your own values. The record should point to the Proxmox host, not to the separate LXC running your other applications.
Once DNS has updated, open the dashboard in your browser.
http://vmhost1.example.comThe dashboard should appear with your configured service links. Clicking a card opens the corresponding application in a new browser tab.
Use the explicit http:// prefix when opening the dashboard. This setup does not provide HTTPS, and browser behavior when the scheme is omitted can vary.
7. Updating the Dashboard
When you want to add, remove, or change a service, edit the HTML file.
nano -w /var/www/host-dashboard.htmlSave your changes and refresh the browser. The Python server reads the file for each request, so you do not need to restart the systemd service.
If you change the listening IP address or port in the Python script, restart the service to apply the change.
systemctl restart dashboard-server.serviceConclusion
That’s it! We now have a simple landing page running directly on the Proxmox host. It uses one HTML file and a small Python script, with systemd handling startup and service recovery.
The dashboard is easy to customize. Add links to the services you use, update the HTML, and refresh your browser. The Proxmox management interface remains available on port 8006, while port 80 serves the dashboard.
This setup is intended for a trusted internal network. The dashboard itself uses HTTP, and the links it displays do not provide additional access controls. Continue to secure the Proxmox management interface and each linked service using your normal network and authentication policies.
