HOMELAB
Deploying Glance as a self-hosted dashboard
A practical Glance deployment behind Nginx Proxy Manager, with Docker and server metrics, service monitoring, API widgets and secrets kept out of YAML.
Most homelab dashboards start as a grid of links and gradually become an operations surface. The useful version shows whether services are reachable, what Docker is doing, how much disk is left and which systems need attention. Glance covers that without a database or a frontend build: one small container reads YAML and renders the dashboard.
This is the deployment I use on a Raspberry Pi. Glance runs behind Nginx Proxy Manager, has no published host port, reads secrets from an environment file and uses both built-in and community widgets. The same structure works on any Docker host.
Directory structure
Keep configuration, assets and deployment metadata together. Splitting large pages into included YAML files is useful later, but one configuration file is easier to validate during the first deployment.
glance/
├── .env
├── compose.yaml
├── assets/
└── config/
└── glance.ymlThe .env file contains API tokens and credentials. Set its mode to 0600 and exclude it from version control:
touch .env
chmod 600 .envGlance supports ${VARIABLE} substitutions anywhere in its configuration. It also supports Docker secrets and reading values from mounted files, which are better choices when the host already has a secret-management workflow.
Run Glance without publishing a port
The reverse proxy and Glance share an external Docker network, so Nginx Proxy Manager can forward directly to glance:8080. There is no reason to bind port 8080 on every interface of the host.
services:
glance:
image: glanceapp/glance:v0.8.5
container_name: glance
restart: unless-stopped
env_file:
- path: .env
format: raw
volumes:
- ./config:/app/config
- ./assets:/app/assets
- /var/run/docker.sock:/var/run/docker.sock:ro
- /:/host:ro
networks:
proxy:
networks:
proxy:
external: truePin the image instead of using latest. A dashboard is not critical infrastructure, but an unexpected configuration change is still a poor reason to lose the page used to diagnose everything else.
The Docker socket enables the container widget. Mounting the socket read-only prevents the socket file itself from being changed, but it does not turn the Docker API into a read-only API. A compromised process can still issue mutating requests. A Docker socket proxy restricted to the endpoints Glance needs is the stronger production setup.
The /host mount makes the real host filesystem visible to the server statistics widget. Without it, disk usage describes the container filesystem and is operationally useless.
Configure the server and page
Set proxied: true so Glance trusts the forwarded scheme and client headers supplied by Nginx Proxy Manager. Then define branding, a restrained theme and up to three columns per page.
server:
proxied: true
assets-path: /app/assets
branding:
app-name: Home Dashboard
logo-text: HD
hide-footer: true
theme:
background-color: 220 16 10
primary-color: 205 76 62
positive-color: 145 54 52
negative-color: 4 72 62
pages:
- name: Home
head-widgets:
- type: search
search-engine: google
new-tab: true
columns:
- size: small
widgets:
- type: clock
hour-format: 24h
- type: weather
location: Warsaw, Poland
units: metric
- size: full
widgets:
- type: monitor
title: Services
cache: 1m
sites:
- title: Grafana
url: https://grafana.example.com
icon: si:grafana
- title: GitLab
url: https://gitlab.example.com
icon: si:gitlabGlance reloads valid YAML changes automatically. If an edit is invalid, it logs the error and continues serving the last working configuration. Environment changes are different: they require a container restart.
Add host and Docker statistics
The server statistics widget can hide every container mount and expose only the mounted host root. The Docker widget discovers containers through the socket and can be customised with Compose labels or an explicit containers map.
- type: server-stats
title: Raspberry Pi
servers:
- type: local
name: Raspberry Pi
hide-mountpoints-by-default: true
mountpoints:
/host:
name: System
hide: false
- type: docker-containers
title: Docker containers
hide-by-default: falseFor multi-container applications, use glance.id on the main service and glance.parent on its database, cache and worker containers. This keeps a Compose stack together instead of presenting every dependency as an unrelated application.
Keep credentials out of the widget
Native and custom API widgets both accept environment substitutions. Pi-hole, for example, has a built-in widget:
PIHOLE_URL=https://pihole.example.com
PIHOLE_TOKEN=replace-me- type: dns-stats
title: Pi-hole
service: pihole
url: ${PIHOLE_URL}
token: ${PIHOLE_TOKEN}Do not commit the real .env. Also remember that normal container environment variables are visible to users who can inspect the container. Docker secrets or a mounted credential file provide a tighter boundary when multiple people administer the host.
Use community widgets for application APIs
Glance’s custom-api widget fetches JSON and renders it with Go templates. The community repository contains reusable widgets for Portainer, Nginx Proxy Manager, qBittorrent and many other services. Custom API widgets in that repository are reviewed by the Glance maintainers, but they remain community-maintained code and should still be read before use.
A compact Portainer widget only needs an API key and endpoint ID:
- type: custom-api
title: Portainer
title-url: ${PORTAINER_URL}
cache: 10m
url: ${PORTAINER_URL}/api/endpoints/${PORTAINER_ENDPOINT_ID}
headers:
X-API-Key: ${PORTAINER_API_KEY}
template: |
{{ if eq .Response.StatusCode 200 }}
<div class="flex justify-between text-center">
<div>
<div class="color-highlight size-h3">
{{ .JSON.Int "Snapshots.0.RunningContainerCount" }}
</div>
<div class="size-h6">RUNNING</div>
</div>
<div>
<div class="color-highlight size-h3">
{{ .JSON.Int "Snapshots.0.StoppedContainerCount" }}
</div>
<div class="size-h6">STOPPED</div>
</div>
</div>
{{ else }}
<p class="color-negative">Portainer API returned {{ .Response.Status }}</p>
{{ end }}Nginx Proxy Manager is slightly more involved. Its widget first posts credentials to /api/tokens, extracts the short-lived token and uses newRequest to fetch proxy hosts. Keep those credentials in .env, and prefer a restricted account if the target application supports one.
The qBittorrent community widget calls the Web API without handling its login cookie. Give the Glance container a fixed Docker IP and whitelist that single /32 address in qBittorrent. Do not whitelist an entire bridge subnet unless every container on it is trusted.
Put Nginx Proxy Manager in front
Create a proxy host with these values:
- Domain:
glance.example.com - Scheme:
http - Forward hostname:
glance - Forward port:
8080 - WebSocket support: enabled
- Block common exploits: enabled
- SSL: a valid certificate, forced HTTPS and HTTP/2
- Access list: none, if the dashboard is intentionally unauthenticated
Point DNS at an address that can actually reach the reverse proxy. In my deployment the record resolves to a Tailscale address. The name exists in public DNS, but the address is reachable only by devices in the tailnet. This is materially different from exposing an unauthenticated dashboard to the public internet.
If the record points at a public IP, enable Glance authentication or put an identity-aware proxy in front of it. A dashboard advertises service names, internal tooling and operational state even when none of its links are directly reachable.
Start and verify
Render the Compose model before starting. This catches malformed environment-file declarations and network names without touching the running stack.
docker compose config --quiet
docker compose pull
docker compose up -dThen check the server and both rendered page endpoints:
docker ps --filter name=glance
docker logs --tail 100 glance
curl -fsS https://glance.example.com/
curl -fsS https://glance.example.com/api/pages/home/content/The page shell can return HTTP 200 while an API widget contains an error, so inspect the rendered content and the Glance logs. Check desktop and mobile layouts as well: community templates can contain their own HTML and CSS, and a widget that looks fine in a full column can overflow a narrow one.
Summary
The deployment is one pinned container, one environment file and one YAML configuration. The choices that make it dependable are less visible: no host port, a shared proxy network, secrets outside versioned YAML, a narrow qBittorrent whitelist and a deliberate decision about who can reach an unauthenticated dashboard.
Start with native monitoring, bookmarks, weather, server statistics and Docker. Add community API widgets one at a time, verify their failure state as carefully as their success state, and keep the page useful when one upstream service is down.
Reference: Glance configuration documentation.