Skip to content

Homepage Dashboard

Status: Deployed ✅ Date: 2026-04-11 URL: dash.exzentcg.com


What is Homepage?

Homepage is a lightweight, self-hosted dashboard that shows all ExzenTCG services at a glance. It has no built-in authentication — access is gated behind Cloudflare Access.

Why we need it: Gives the team a single page to see every service, its URL, and its status. No more bookmarking individual URLs or asking "what's the link for n8n?"


Deployment Details

Property Value
CT ID 103
Hostname homepage
IP 192.168.0.53
Port 3000
Docker image ghcr.io/gethomepage/homepage:latest
RAM 256 MB
Swap 256 MB
Disk 4 GB
DNS 1.1.1.1 (set via pct set 103 --nameserver 1.1.1.1)
Public URL https://dash.exzentcg.com
Auth Cloudflare Access (owner-only, 7-day session)
Snapshot initial-deploy

Deployment Log

Step 1 — Create LXC

pct create 103 local:vztmpl/debian-12-standard_12.12-1_amd64.tar.zst \
  --hostname homepage \
  --cores 1 \
  --memory 256 \
  --swap 256 \
  --rootfs local-lvm:4 \
  --net0 name=eth0,bridge=vmbr0,ip=192.168.0.53/24,gw=192.168.0.1 \
  --nameserver 192.168.0.1 \
  --features nesting=1 \
  --onboot 1 \
  --start 1

Step 2 — Install Docker + Deploy

apt update && apt upgrade -y && apt install curl -y
curl -fsSL https://get.docker.com | sh
# Docker 29.4.0 installed
mkdir -p /opt/homepage/config && cd /opt/homepage

Docker Compose (/opt/homepage/docker-compose.yml):

services:
  homepage:
    image: ghcr.io/gethomepage/homepage:latest
    container_name: homepage
    restart: unless-stopped
    ports:
      - "192.168.0.53:3000:3000"
    volumes:
      - ./config:/app/config
    environment:
      - HOMEPAGE_ALLOWED_HOSTS=dash.exzentcg.com,192.168.0.53:3000

Note

First docker compose up failed — ghcr.io DNS lookup failed on router DNS (192.168.0.1). Fixed with pct set 103 --nameserver 1.1.1.1 from the Proxmox host, then rebooted the container.

Step 3 — Configuration

/opt/homepage/config/services.yaml (segregated by access method):

- "Web Services (via Cloudflare Access)":
    - n8n:
        icon: n8n.png
        href: https://n8n.exzentcg.com
        description: Workflow automation
        siteMonitor: http://192.168.0.52:5678
        widget:
          type: customapi
          url: http://192.168.0.52:5678/api/v1/workflows?limit=100&active=true
          headers:
            X-N8N-API-KEY: "{{HOMEPAGE_VAR_N8N_KEY}}"
          refreshInterval: 30000
          mappings:
            - field:
                data: length
              label: Active Workflows
              format: number
    - Dashboard:
        icon: homepage.png
        href: https://dash.exzentcg.com
        description: Service dashboard
    - Docs:
        icon: mdi-book-open-variant
        href: https://docs.exzentcg.com
        description: Team documentation
        ping: https://docs.exzentcg.com
    - Telegram App:
        icon: si-telegram
        href: https://telegram2.exzentcg.com
        description: Telegram Store (Vercel)
        ping: https://telegram2.exzentcg.com

- "Internal Services (Tailscale / LAN only)":
    - Proxmox:
        icon: proxmox.png
        href: https://192.168.0.200:8006
        description: Hypervisor management
        widget:
          type: proxmox
          url: https://192.168.0.200:8006
          username: root@pam!widget
          password: "{{HOMEPAGE_VAR_PROXMOX_TOKEN}}"
          node: pve
          fields: ["vms", "lxc", "resources.cpu", "resources.mem"]
    - Nginx Proxy Manager:
        icon: nginx-proxy-manager.png
        href: http://192.168.0.51:81
        description: Reverse proxy
        widget:
          type: npm
          url: http://192.168.0.51:81
          username: "{{HOMEPAGE_VAR_NPM_USER}}"
          password: "{{HOMEPAGE_VAR_NPM_PASS}}"
    - Cloudflare Tunnel:
        icon: cloudflare.png
        href: https://dash.cloudflare.com
        description: exzentcg-homelab
        widget:
          type: cloudflared
          accountid: "{{HOMEPAGE_VAR_CF_ACCOUNT}}"
          tunnelid: 72dbcbd0-d7d9-41d4-b55e-87e5eb9b27ad
          key: "{{HOMEPAGE_VAR_CF_TOKEN}}"
    - Home Router:
        icon: mdi-router-wireless
        href: http://192.168.0.1
        description: D-Link admin panel
    - Intel AMT:
        icon: mdi-chip
        href: http://192.168.0.200:16992
        description: Out-of-band management (MeshCommander)

Note

Router and AMT are accessible remotely via Tailscale subnet routing (see Decision D7 in Known Issues & Decisions). The Proxmox host advertises 192.168.0.0/24 as a subnet route.

/opt/homepage/config/bookmarks.yaml:

- Business:
    - Shopee Seller Centre:
        - icon: si-shopee
          href: https://seller.shopee.sg
          description: E-commerce marketplace
    - Lazada Seller Centre:
        - icon: si-lazada
          href: https://sellercenter.lazada.sg
          description: E-commerce marketplace
    - Telegram App:
        - icon: si-telegram
          href: https://telegram2.exzentcg.com
          description: Telegram Store (Vercel)

- Dev & Ops:
    - GitHub:
        - icon: si-github
          href: https://github.com/ExzenTCG
          description: Source code & docs repo
    - Cloudflare:
        - icon: si-cloudflare
          href: https://dash.cloudflare.com
          description: DNS, tunnel, zero trust
    - Vercel:
        - icon: si-vercel
          href: https://vercel.com/dashboard
          description: Telegram Store hosting
    - Google Cloud:
        - icon: si-googlecloud
          href: https://console.cloud.google.com
          description: OAuth & API credentials
    - Google Sheets:
        - icon: si-googlesheets
          href: https://docs.google.com/spreadsheets
          description: Business spreadsheets

/opt/homepage/config/widgets.yaml:

- greeting:
    text_size: xl
    text: ExzenTCG Hub
- search:
    provider: google
    target: _blank
- datetime:
    locale: en-SG
    format:
      dateStyle: medium
      timeStyle: short

/opt/homepage/config/settings.yaml:

title: ExzenTCG Hub
favicon: data:image/x-icon;base64,<base64 encoded favicon.ico>
theme: dark
color: neutral
headerStyle: clean
statusStyle: dot
quicklaunch:
  searchDescriptions: true
layout:
  "Web Services (via Cloudflare Access)":
    style: row
    columns: 4
  "Internal Services (Tailscale / LAN only)":
    style: row
    columns: 3
  Business:
    style: row
    columns: 3
  "Dev & Ops":
    style: row
    columns: 3

/opt/homepage/docker-compose.yml (environment variables for widget API keys):

environment:
  - HOMEPAGE_ALLOWED_HOSTS=dash.exzentcg.com,192.168.0.53:3000
  - HOMEPAGE_VAR_PROXMOX_TOKEN=<Proxmox API token secret>
  - HOMEPAGE_VAR_CF_TOKEN=<Cloudflare API token>
  - HOMEPAGE_VAR_CF_ACCOUNT=<Cloudflare account ID>
  - HOMEPAGE_VAR_NPM_USER=<NPM admin email>
  - HOMEPAGE_VAR_NPM_PASS=<NPM admin password>
  - HOMEPAGE_VAR_N8N_KEY=<n8n API key>

Note

The favicon must be base64-encoded inline because Cloudflare Access blocks unauthenticated requests to the favicon URL. Generated with: FAVICON_B64=$(base64 -w0 /opt/homepage/config/images/favicon.ico) then embedded in settings.yaml.

Note

Widget API credentials are stored as environment variables in docker-compose.yml and referenced in services.yaml via {{HOMEPAGE_VAR_*}} syntax. Never hardcode credentials directly in the YAML config files.

Step 4 — Firewall

CT 103 firewall/etc/pve/firewall/103.fw:

[OPTIONS]
enable: 1

[RULES]
IN ACCEPT -source +edge_gw -p tcp -dport 3000     # Only edge-gateway can reach Homepage
IN ACCEPT -source +admin_desktop -p tcp -dport 22  # SSH from admin
IN ACCEPT -source +admin_desktop -p tcp -dport 3000 # Direct LAN access for dev
IN DROP                                             # Block all other inbound
OUT ACCEPT -dest +router_gw -p udp -dport 53       # DNS
OUT ACCEPT -dest +router_gw -p tcp -dport 53       # DNS
OUT ACCEPT -p udp -dport 53                         # Public DNS fallback (1.1.1.1)
OUT ACCEPT -dest 192.168.0.200 -p tcp -dport 8006  # Proxmox API for widget
OUT ACCEPT -dest 192.168.0.51 -p tcp -dport 81     # NPM widget API access
OUT ACCEPT -dest 192.168.0.52 -p tcp -dport 5678   # n8n widget API access
OUT DROP -dest +lan_subnet                          # Lateral movement prevention
OUT ACCEPT -p tcp -dport 443                        # HTTPS (icons, API status checks)

Note

firewall=1 must be set on the network interface, not the container: pct set 103 --net0 name=eth0,bridge=vmbr0,ip=192.168.0.53/24,gw=192.168.0.1,firewall=1

Edge-gateway 101.fw — two changes: - Comment updated from "Proxy to biz-app (Phase 2)" to "Proxy to homepage" on OUT ACCEPT -dest 192.168.0.53 - Added: IN ACCEPT -source 192.168.0.53 -p tcp -dport 81 (Homepage widget access to NPM admin)

n8n-app 102.fw — added: IN ACCEPT -source 192.168.0.53 -p tcp -dport 5678 (Homepage widget access to n8n API)

Proxmox host host.fw — added: IN ACCEPT -source 192.168.0.53 -p tcp -dport 8006 (Homepage widget access to Proxmox API)

Firewall test: curl -s --connect-timeout 3 http://192.168.0.16 → HTTP 000 ✅ (LAN blocked)

Step 5 — NPM Proxy Host

Field Value
Domain Names dash.exzentcg.com
Scheme http
Forward Hostname / IP 192.168.0.53
Forward Port 3000
Block Common Exploits
Websockets Support
SSL Certificate None

Step 6 — Cloudflare Tunnel Route

Published application route added to exzentcg-homelab tunnel:

Field Value
Subdomain dash
Domain exzentcg.com
Service http://localhost:80

DNS CNAME auto-created: dash.exzentcg.com → 72dbcbd0-...cfargotunnel.com

Step 7 — Cloudflare Access

Field Value
Application name dashboard
Subdomain dash
Domain exzentcg.com
Session Duration 7 days
Policy owner-only (Allow, Emails: exzensg@gmail.com)

Step 8 — Verification

  • https://dash.exzentcg.com → Cloudflare Access login → Homepage dashboard ✅
  • All service cards visible and clickable ✅

Step 9 — Snapshot

pct snapshot 103 initial-deploy --description "Homepage dashboard initial deployment"

Customisation

Homepage is configured entirely via YAML files in /opt/homepage/config/:

File Purpose
services.yaml Service cards (name, icon, URL, description)
bookmarks.yaml Quick-access bookmark links by category
widgets.yaml Top bar widgets (weather, clock, search, etc.)
settings.yaml Theme, layout, background, title
custom.css Full CSS override

To add a new service card, edit services.yaml on CT 103:

pct enter 103
nano /opt/homepage/config/services.yaml
# Homepage auto-reloads config — no restart needed

Troubleshooting

Problem Cause Fix
502 Bad Gateway Homepage container down pct enter 103 && cd /opt/homepage && docker compose up -d
Icons not loading Firewall blocking HTTPS outbound Check OUT ACCEPT -p tcp -dport 443 in 103.fw
"Host not allowed" error HOMEPAGE_ALLOWED_HOSTS missing the domain Add domain to env var in docker-compose.yml, restart
DNS resolution failed during docker pull Container using router DNS pct set 103 --nameserver 1.1.1.1 from Proxmox host
Proxmox widget shows dashes CT 103 can't reach Proxmox API Check OUT ACCEPT -dest 192.168.0.200 -p tcp -dport 8006 in 103.fw AND IN ACCEPT -source 192.168.0.53 in host.fw
Proxmox widget 401 error Wrong token ID or secret Verify with: curl -sk -H 'Authorization: PVEAPIToken=root@pam!widget:<secret>' 'https://192.168.0.200:8006/api2/json/version'
Cloudflare widget t.result is null Wrong/expired API token Rotate token in Cloudflare dashboard, update HOMEPAGE_VAR_CF_TOKEN in docker-compose.yml, docker compose up -d --force-recreate
Favicon not loading Cloudflare Access blocks the request Must be base64-encoded inline in settings.yaml