Back to blog

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.

layout.txt
glance/
├── .env
├── compose.yaml
├── assets/
└── config/
    └── glance.yml

The .env file contains API tokens and credentials. Set its mode to 0600 and exclude it from version control:

secrets.sh
touch .env
chmod 600 .env

Glance 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.

compose.yaml
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: true

Pin 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.

glance.yml
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:gitlab

Glance 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.

infrastructure.yml
- 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: false

For 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:

.env
PIHOLE_URL=https://pihole.example.com
PIHOLE_TOKEN=replace-me
pihole.yml
- 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:

portainer.yml
- 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.

deploy.sh
docker compose config --quiet
docker compose pull
docker compose up -d

Then check the server and both rendered page endpoints:

verify.sh
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.