Dashboard
Docker Compose services that show links, widgets, and live status for the other self-hosted services and machines on the network: Apache in front as reverse proxy and login gate, then Glance and Homepage behind it. They cover Emby, Jellyfin, Navidrome, Devops Server, its PyPI mirror, Code Server, qBittorrent, Nextcloud, Vikunja, Nginx Proxy Manager, OpenWRT, S3, Vault, and per-machine Glances stats.
Architecture
client → apache:10333 (form auth, session cookie)
├─ /my-dashboard* → glance:10330 (proxied, base-url /my-dashboard)
├─ /my-dashboardmanifest.json → glance:10330/manifest.json (PWA manifest workaround, see httpd.conf)
├─ /login.html, /dologin, /dologout, /file-storage → served by Apache itself
└─ / (everything else) → homepage:10331
Apache (stack/apache/httpd.conf) is the only container with a host port (10333:10333 for the authenticated port, 10334:10334 for the LAN-only port). glance and homepage are reachable only from the dashboard bridge network.
mod_auth_form puts every path except /login.html, /dologin, /dologout, and the PWA manifest behind /usr/local/apache2/conf/.htpasswd. stack/apache/entrypoint.sh writes that file at container start from DASHBOARD_VAR_USER and DASHBOARD_VAR_PASSWORD, and stack/apache/htdocs/login.html is the login page. /file-storage (WebDAV) uses plain HTTP Basic auth against the same .htpasswd, because WebDAV clients can't fill in an HTML form or keep a session cookie.
On 10333, mod_remoteip takes the visitor's address from X-Forwarded-For when the connection comes from a loopback/private address (Nginx Proxy Manager) and skips Cloudflare's edge ranges, so access logs show the real client IP. Port 10334 ignores that header and checks the direct peer.
Port 10334 serves the same content with no login, but only to clients in 10.8.33.0/24 and 10.8.34.0/24 (hardcoded in stack/apache/httpd.conf); any other source gets 403. Point the reverse proxy and Cloudflare at 10333 only, and keep 10334 unforwarded and unreachable from outside the LAN.
entrypoint.sh also picks a new random SessionCryptoPassphrase on every start to encrypt the session cookie. Restarting the container logs everyone out.
- Glance (
stack/glance/config/) renders the "Applications" page (bookmark groups for this dashboard's own links, managed cloud/domain admin consoles, this stack's self-hosted services, and a Nginx Proxy Manager widget listing every enabled proxy host) and the "Feed" page (weather, markets, Twitch, a Vikunja taskboard, Reddit/Hacker News/Lobsters, videos, GitHub notifications, recent Devops Server repo activity, and a GitHub releases tracker). - Homepage (
stack/homepage/config/) renders the service/machine tree: one entry per self-hosted app underApplications, and one entry per physical host underMachinesbuilt fromglances-widget metric blocks (host info, CPU, memory, GPU, sensors, filesystems, disks, network, processes, containers).
Commands
docker-compose.yml, apache/, glance/, and homepage/ all live under stack/, so docker compose commands run from there.
Run the stack:
cd stack
docker compose up -d
docker compose pull && docker compose up -d # pick up new pinned image tags
docker compose logs -f <apache|glance|homepage>
Environment variables supply every DASHBOARD_VAR_* / MACHINE_* / DOCKER_MACHINE_TLS_* value the compose file interpolates. See the table below for what each variable is. For local docker compose runs, create a stack/.env file with these variables. For remote deployments, the Python deployer reads environment variables from the current process and generates the .env file on the remote machine. Importing the dashboard package also loads the nearest .env above src/dashboard/ (the repo root's .env in a checkout), without overriding variables the process already has. The dav-data volume binds to the fixed host path /srv/dashboard/dav, which must exist (sudo mkdir -p /srv/dashboard/dav) on whichever host runs the stack.
Deploy to a remote machine:
DASHBOARD_REMOTE_MACHINE=<host> python -m dashboard.deployer
The deployer copies stack/ to /app/dashboard/stack on DASHBOARD_REMOTE_MACHINE (default rb5-m3) over SSH/SFTP. The deployer connects with paramiko, which doesn't read ~/.ssh/config: the hostname must resolve through DNS or /etc/hosts, the login user is your local username, and authentication uses your SSH agent or the default keys under ~/.ssh/. The deployer:
- Validates all required environment variables from the process environment.
- Checks for an existing stack on the remote. If the compose file is present but
.envis missing, writes.envfirst (docker compose downneeds it for variable interpolation), then runsdocker compose --progress plain down -v(removes volumes). - Removes any existing stack directory on the remote.
- Creates
/app/dashboard/stackand/srv/dashboard/davdirectories. WebDAV data lives outside the app directory, so redeploys leave it alone. - Uploads
stack/via SFTP. - Generates and writes
.envto the remote stack directory with all validated environment variables. - Rebuilds the images with
docker compose --progress plain build --no-cacheand starts the stack withdocker compose --progress plain up -d --force-recreate.
Using the deployer:
uv --offline run --no-sync --no-progress dashboard-deployer
# With explicit remote machine
DASHBOARD_REMOTE_MACHINE=rb5-m3 dashboard-deployer
Development
The pre-commit hook runs gitleaks (must be installed and
on PATH) to scan for secrets. Enable it once per clone:
chmod +x .git-hooks/*
git config --local core.hooksPath .git-hooks
A .gitea/workflows/gitleaks.yml CI job runs the same scan on every push, pull request, and a
daily schedule.
Environment variables
docker-compose.yml maps each DASHBOARD_VAR_* and MACHINE_* value Homepage uses from .env to a HOMEPAGE_VAR_* environment variable, which Homepage's config files read as {{HOMEPAGE_VAR_*}}. The glance service gets the values it needs (DASHBOARD_VAR_*, MACHINE_OPENWRT_IP, MACHINE_S1_DEV_IP, GH_PROD_API_TOKEN, DASHBOARD_GLANCE_CACHE_SEC) under the same names in its environment: block and reads them as ${NAME} in its YAML.
Every variable below is required. docker-compose.yml interpolates each one with ${VAR:?error}, so docker compose up fails if one is missing from .env. Without that, the widget would render blank.
Service credentials and endpoints
| Variable | Description |
|---|---|
DASHBOARD_VAR_EMBY_API_ENDPOINT | Emby server API endpoint, like https://127.0.0.1:3466. |
DASHBOARD_VAR_EMBY_SERVER | Emby server address, like 127.0.0.1. |
DASHBOARD_VAR_EMBY_API_PASSWORD | Emby server API password. |
DASHBOARD_VAR_DEVOPS_SERVER_HOST | Devops Server server address, like 127.0.0.1. |
DASHBOARD_VAR_DEVOPS_SERVER_API_ENDPOINT | Devops Server server API endpoint, like https://127.0.0.1:3000. |
DASHBOARD_VAR_DEVOPS_SERVER_API_PASSWORD | Devops Server server API password. |
DASHBOARD_VAR_PYPI_MIRROR_HOST | Devops Server PyPI mirror (devpi) server address, like 127.0.0.1. |
DASHBOARD_VAR_PYPI_MIRROR_API_ENDPOINT | Devops Server PyPI mirror (devpi) API endpoint, like http://127.0.0.1:5502. |
DASHBOARD_VAR_JELLYFIN_API_ENDPOINT | Jellyfin server API endpoint, like https://127.0.0.1:3459. |
DASHBOARD_VAR_JELLYFIN_SERVER | Jellyfin server address, like 127.0.0.1. |
DASHBOARD_VAR_JELLYFIN_API_PASSWORD | Jellyfin server API password. |
DASHBOARD_VAR_NAVIDROME_HOST | Navidrome server address, like 127.0.0.1. |
DASHBOARD_VAR_NAVIDROME_URL | Navidrome server URL, like http://127.0.0.1:7644. |
DASHBOARD_VAR_NAVIDROME_USERNAME | Navidrome username. |
DASHBOARD_VAR_NAVIDROME_RANDOM_SALT | Random salt used to compute DASHBOARD_VAR_NAVIDROME_PASSWORD_MD5. |
DASHBOARD_VAR_NAVIDROME_PASSWORD_MD5 | MD5 hash of the Navidrome password concatenated with DASHBOARD_VAR_NAVIDROME_RANDOM_SALT (Subsonic API token auth). |
DASHBOARD_VAR_QBITTORRENT_ENDPOINT | qBittorrent WebUI endpoint, like https://127.0.0.1:9077. |
DASHBOARD_VAR_QBITTORRENT_SERVER | qBittorrent server address, like 127.0.0.1. |
DASHBOARD_VAR_QBITTORRENT_API_KEY | qBittorrent WebUI API key. |
DASHBOARD_VAR_NEXTCLOUD_SERVER | Nextcloud server address, like 127.0.0.1. |
DASHBOARD_VAR_NEXTCLOUD_ENDPOINT | Nextcloud server API endpoint, like https://127.0.0.1:8477. |
DASHBOARD_VAR_NEXTCLOUD_TOKEN | Nextcloud server API token. |
DASHBOARD_VAR_NGINX_PROXY_MANAGER_ENDPOINT | Nginx Proxy Manager API endpoint, like https://127.0.0.1:8181. |
DASHBOARD_VAR_NGINX_PROXY_MANAGER_SERVER | Nginx Proxy Manager server address, like 127.0.0.1. |
DASHBOARD_VAR_NGINX_PROXY_MANAGER_USERNAME | Nginx Proxy Manager username. |
DASHBOARD_VAR_NGINX_PROXY_MANAGER_PASSWORD | Nginx Proxy Manager password. |
DASHBOARD_VAR_OPENWRT_ENDPOINT | OpenWRT API endpoint, like https://192.168.1.1:8080. Also used as the r1-tpla9v6 machine's link/monitor URL in stack/homepage/config/services.yaml. |
DASHBOARD_VAR_OPENWRT_SERVER | OpenWRT server address, like 192.168.1.1. |
DASHBOARD_VAR_OPENWRT_API_USERNAME | OpenWRT API username. |
DASHBOARD_VAR_OPENWRT_API_PASSWORD | OpenWRT API password. |
MACHINE_OPENWRT_IP | OpenWRT router address, like 192.168.1.1. Used as the "Only On Site" OpenWrt bookmark link in stack/glance/config/applications/bookmarks-blr-home.yml. |
DASHBOARD_VAR_S3_CONSOLE_ENDPOINT | S3 Console endpoint, like https://127.0.0.1:9545. |
DASHBOARD_VAR_S3_SERVER | S3 server address, like 127.0.0.1. |
DASHBOARD_VAR_VAULT_ENDPOINT | Vault Secrets Manager endpoint, like https://127.0.0.1:9744. |
DASHBOARD_VAR_VAULT_SERVER | Vault Secrets Manager server address, like 127.0.0.1. |
DASHBOARD_VAR_CODE_SERVER_ENDPOINT | Code Server endpoint, like http://127.0.0.1:7745. |
DASHBOARD_VAR_CODE_SERVER_IP | Code Server address, like 127.0.0.1. |
DASHBOARD_VAR_VIKUJAN_ENDPOINT | Vikunja server endpoint, like http://127.0.0.1:5773. Also used to build the Feed page's Vikunja tasks widget API URL. |
DASHBOARD_VAR_VIKUJAN_SERVER | Vikunja server address, like 127.0.0.1. |
DASHBOARD_VAR_VIKUJAN_API_KEY | Vikunja API token used by the Feed page's Vikunja tasks widget. |
GH_PROD_API_TOKEN | Read-only GitHub API token used by the Feed page's releases and GitHub notifications widgets. |
Dashboard auth
Consumed by stack/apache/entrypoint.sh to generate /usr/local/apache2/conf/.htpasswd at container startup, gating every dashboard path behind a single login.
| Variable | Description |
|---|---|
DASHBOARD_VAR_USER | Username for the Apache login gate. |
DASHBOARD_VAR_PASSWORD | Password for the Apache login gate. |
Glance cache
Sourced from .env and referenced as ${DASHBOARD_GLANCE_CACHE_SEC}s by the cache property of Glance widgets that poll an external API or feed (Nginx Proxy Manager, Devops activity, GitHub notifications, Vikunja tasks, Hacker News, Lobsters, RSS, Reddit, Twitch, videos, releases).
| Variable | Description |
|---|---|
DASHBOARD_GLANCE_CACHE_SEC | Default cache duration, in seconds, for cacheable Glance widgets. |
Homepage refresh rate
Sourced from .env and referenced as {{HOMEPAGE_VAR_REFRESH_RATE_MS}} by the refreshInterval property of glances widgets in stack/homepage/config/services.yaml and the refresh property of the resources widget in stack/homepage/config/widgets.yaml.
| Variable | Description |
|---|---|
HOMEPAGE_VAR_REFRESH_RATE_MS | Default pull rate, in milliseconds, for Homepage's live stats widgets. |
Machines
Each physical host has an _IP / _DOCKER_TLS_PORT / _GLANCES_PASSWORD set. These feed the Machines entries and glances widgets in stack/homepage/config/services.yaml, and the remote Docker socket targets in stack/homepage/config/docker.yaml. The machine display name (S1 Dev, S1 Home, RB5 M3, Sash M1) must stay in sync with the variable name when renaming or adding a machine. MACHINE_S1_DEV_IP also builds the "Open WebUI - Only On Site" bookmark link in stack/glance/config/applications/bookmarks-blr-home.yml.
| Variable | Description |
|---|---|
MACHINE_S1_DEV_IP | IP address of the "S1 Dev" machine. |
MACHINE_S1_DEV_DOCKER_TLS_PORT | mTLS Docker daemon port on "S1 Dev". |
MACHINE_S1_DEV_GLANCES_PASSWORD | Glances widget password for "S1 Dev". |
MACHINE_S1_HOME_IP | IP address of the "S1 Home" machine. |
MACHINE_S1_HOME_DOCKER_TLS_PORT | mTLS Docker daemon port on "S1 Home". |
MACHINE_S1_HOME_GLANCES_PASSWORD | Glances widget password for "S1 Home". |
MACHINE_RB5_M3_IP | IP address of the "RB5 M3" machine. |
MACHINE_RB5_M3_DOCKER_TLS_PORT | mTLS Docker daemon port on "RB5 M3". |
MACHINE_RB5_M3_GLANCES_PASSWORD | Glances widget password for "RB5 M3". |
MACHINE_SASH_M1_IP | IP address of the "Sash M1" machine. |
MACHINE_SASH_M1_DOCKER_TLS_PORT | mTLS Docker daemon port on "Sash M1". |
MACHINE_SASH_M1_GLANCES_PASSWORD | Glances widget password for "Sash M1". |
Docker TLS certificates
The homepage-certs service decodes these into PEM files on the homepage-certs-data named volume, which is mounted into the homepage container at /app/config/certs/, where stack/homepage/config/docker.yaml references them (certs/docker_tls_*.pem) to authenticate to the remote machines' Docker daemons over mTLS.
| Variable | Description |
|---|---|
DOCKER_MACHINE_TLS_CA_BASE64 | Base64-encoded Docker mTLS CA certificate. |
DOCKER_MACHINE_TLS_CERT_BASE64 | Base64-encoded Docker mTLS client certificate. |
DOCKER_MACHINE_TLS_KEY_BASE64 | Base64-encoded Docker mTLS client key. |