Dashboard
    Go binary

    Self-Host Glance on a VPS

    A tiny Go-based dashboard that pulls RSS, GitHub releases, weather, server stats, Docker status, calendars, and dozens of other sources into a single page. Idles at a few MB of RAM — almost ideal for a budget VPS.

    At a Glance

    Projectglanceapp/glance
    LanguageGo (single static binary)
    Recommended PlanAny RamNode KVM with ≥1 GB RAM (idles <100 MB)
    OSUbuntu 24.04 LTS (Debian 12 / AlmaLinux 9 also work)
    Setup Time~30 minutes
    1

    Initial Server Hardening

    Update + non-root user
    apt update && apt upgrade -y
    apt install -y curl wget vim ufw fail2ban
    
    adduser deploy
    usermod -aG sudo deploy
    mkdir -p /home/deploy/.ssh
    cp /root/.ssh/authorized_keys /home/deploy/.ssh/
    chown -R deploy:deploy /home/deploy/.ssh
    chmod 700 /home/deploy/.ssh
    chmod 600 /home/deploy/.ssh/authorized_keys
    /etc/ssh/sshd_config
    PermitRootLogin no
    PasswordAuthentication no
    PubkeyAuthentication yes
    Reload SSH + firewall
    systemctl reload ssh
    
    ufw default deny incoming
    ufw default allow outgoing
    ufw allow OpenSSH
    ufw allow 80/tcp
    ufw allow 443/tcp
    ufw enable

    Open a second terminal and confirm deploy SSH works before closing the root session. Glance listens on 8080 internally — keep that bound to localhost.

    2

    Install Docker

    Docker's official APT repo
    sudo install -m 0755 -d /etc/apt/keyrings
    sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
    sudo chmod a+r /etc/apt/keyrings/docker.asc
    
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \
      | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    
    sudo apt update
    sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
    sudo usermod -aG docker $USER

    Log out + back in, then verify with docker run --rm hello-world.

    3

    Deploy with Docker Compose

    Layout + starter config
    sudo mkdir -p /opt/glance
    sudo chown $USER:$USER /opt/glance
    cd /opt/glance
    mkdir config
    
    wget -O config/glance.yml \
      https://raw.githubusercontent.com/glanceapp/glance/refs/heads/main/docs/glance.yml
    /opt/glance/compose.yaml
    services:
      glance:
        image: glanceapp/glance:latest
        container_name: glance
        restart: unless-stopped
        ports:
          - "127.0.0.1:8080:8080"
        volumes:
          - ./config:/app/config
          - /etc/timezone:/etc/timezone:ro
          - /etc/localtime:/etc/localtime:ro
        environment:
          - TZ=America/New_York
    Bring it up
    docker compose up -d
    docker compose logs -f
    curl -I http://127.0.0.1:8080   # should return 200

    127.0.0.1:8080:8080 binds Glance to loopback only — NGINX reaches it fine, external scanners cannot.

    4

    Build a Sensible Configuration

    The default glance.yml is a feature demo with noisy external API hits. Replace with a leaner config:

    config/glance.yml
    server:
      host: 0.0.0.0
      port: 8080
      proxied: true   # REQUIRED when behind NGINX
    
    theme:
      background-color: 240 8 9
      primary-color: 99 100 255
      contrast-multiplier: 1.1
    
    pages:
      - name: Home
        columns:
          - size: small
            widgets:
              - type: calendar
                first-day-of-week: monday
    
              - type: weather
                location: Norfolk, Virginia, United States
                units: imperial
                hour-format: 12h
    
          - size: full
            widgets:
              - type: hacker-news
                limit: 15
                collapse-after: 5
    
              - type: releases
                show-source-icon: true
                token: ${GITHUB_TOKEN}
                repositories:
                  - glanceapp/glance
                  - go-gitea/gitea
                  - caddyserver/caddy
                  - nginx/nginx
    
              - type: rss
                limit: 10
                collapse-after: 3
                cache: 3h
                feeds:
                  - url: https://www.ramnode.com/blog/feed/
                    title: RamNode Blog
    
          - size: small
            widgets:
              - type: server-stats
                servers:
                  - type: local
                    name: VPS

    proxied: true is not optional behind NGINX — without it Glance reads Host/remote IPs incorrectly and any base-URL handling breaks. Without a GitHub token, GitHub allows just 60 unauthenticated requests/hour.

    GitHub token via .env
    echo "GITHUB_TOKEN=ghp_yourtokenhere" > /opt/glance/.env
    chmod 600 /opt/glance/.env

    Then propagate it in compose env:

    compose.yaml env additions
        environment:
          - TZ=America/New_York
          - GITHUB_TOKEN=${GITHUB_TOKEN}

    Generate at github.com/settings/tokens with public_repo scope only. Reload with docker compose restart. Note: Reddit blocks unauthenticated API from datacenter IPs — either register a Reddit app for app-auth or skip the Reddit widget.

    5

    NGINX Reverse Proxy + TLS

    Install
    sudo apt install -y nginx certbot python3-certbot-nginx
    /etc/nginx/sites-available/glance.conf
    server {
        listen 80;
        listen [::]:80;
        server_name glance.yourdomain.com;
    
        location / {
            proxy_pass http://127.0.0.1:8080;
            proxy_http_version 1.1;
    
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Forwarded-Host $host;
    
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
    
            proxy_read_timeout 60s;
            proxy_connect_timeout 10s;
        }
    }
    Enable + cert
    sudo ln -s /etc/nginx/sites-available/glance.conf /etc/nginx/sites-enabled/
    sudo rm -f /etc/nginx/sites-enabled/default
    sudo nginx -t
    sudo systemctl reload nginx
    
    sudo certbot --nginx -d glance.yourdomain.com --redirect --agree-tos -m you@yourdomain.com -n
    6

    Add Authentication

    Glance is now public. Either accept it (RSS, public releases, weather are non-sensitive) or enable built-in auth.

    Generate secret + password hash
    docker run --rm glanceapp/glance secret:make
    docker run --rm glanceapp/glance password:hash 'your-strong-password-here'
    Add to top of config/glance.yml
    auth:
      secret-key: <paste secret here>
      users:
        admin:
          password-hash: <paste hash here>
    Restart
    docker compose restart

    Per-user views aren't supported as of this writing — every authenticated user sees the same dashboard. NGINX-level basic auth also works (htpasswd -c /etc/nginx/.htpasswd admin) but some browser-fetch widgets choke on the basic auth challenge — built-in auth is usually smoother.

    7

    Updates

    Two commands
    cd /opt/glance
    docker compose pull
    docker compose up -d

    For production, pin to a tag like glanceapp/glance:v0.7.0 instead of :latest.

    8

    Backups

    /etc/cron.daily/glance-backup
    #!/bin/bash
    set -e
    BACKUP_DIR=/var/backups/glance
    mkdir -p "$BACKUP_DIR"
    TS=$(date +%F)
    tar -czf "$BACKUP_DIR/glance-config-$TS.tar.gz" -C /opt/glance config
    find "$BACKUP_DIR" -name 'glance-config-*.tar.gz' -mtime +14 -delete
    Make executable
    sudo chmod +x /etc/cron.daily/glance-backup

    No DB to dump, no cache that matters — a flat archive of the config dir is the entire backup story. Push offsite with restic or rclone.

    9

    Alternative: Native Binary with systemd

    Skip Docker entirely on very small VPS plans where the daemon's overhead is unwelcome.

    Install binary + service user
    sudo mkdir -p /opt/glance
    cd /opt/glance
    sudo wget -O glance.tar.gz https://github.com/glanceapp/glance/releases/latest/download/glance-linux-amd64.tar.gz
    sudo tar -xzf glance.tar.gz
    sudo chmod +x glance
    sudo rm glance.tar.gz
    
    sudo useradd --system --no-create-home --shell /usr/sbin/nologin glance
    sudo chown -R glance:glance /opt/glance

    Drop your glance.yml directly in /opt/glance/glance.yml (the binary looks for it beside itself, not in a subdir).

    /etc/systemd/system/glance.service
    [Unit]
    Description=Glance dashboard
    After=network-online.target
    Wants=network-online.target
    
    [Service]
    Type=simple
    User=glance
    Group=glance
    WorkingDirectory=/opt/glance
    ExecStart=/opt/glance/glance --config /opt/glance/glance.yml
    Restart=on-failure
    RestartSec=5
    
    # Hardening
    NoNewPrivileges=true
    PrivateTmp=true
    ProtectSystem=full
    ProtectHome=true
    ProtectKernelTunables=true
    ProtectKernelModules=true
    ProtectControlGroups=true
    
    [Install]
    WantedBy=multi-user.target
    Enable
    sudo systemctl daemon-reload
    sudo systemctl enable --now glance
    sudo systemctl status glance

    The same NGINX server block works unchanged. Updates: stop service → replace binary → start service.

    Troubleshooting

    • 502 Bad Gateway: container or service not running, or bound to a different port. Check docker compose ps / systemctl status glance.
    • All widgets show errors: almost always a DNS or rate-limit issue. Pi-hole / AdGuard Home users hit this on the burst of DNS queries Glance makes during initial widget refresh — raise the resolver rate limit.
    • Reddit widget 403: Reddit blocks unauthenticated API from datacenter IPs. Register a Reddit app for app-auth or remove the widget.
    • Releases widget rate-limited: add a GITHUB_TOKEN. Without it: 60 req/hour for the whole instance.
    • Login loop after enabling auth: proxied: true missing from the server block — without forwarded headers, the cookie gets rejected as non-HTTPS.
    • Subpath assets 404: need both base-url: /glance in the config and a proxy that strips the prefix (proxy_pass http://127.0.0.1:8080/; with trailing slash).