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
| Project | glanceapp/glance |
| Language | Go (single static binary) |
| Recommended Plan | Any RamNode KVM with ≥1 GB RAM (idles <100 MB) |
| OS | Ubuntu 24.04 LTS (Debian 12 / AlmaLinux 9 also work) |
| Setup Time | ~30 minutes |
Initial Server Hardening
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_keysPermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yessystemctl reload ssh
ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enableOpen a second terminal and confirm deploy SSH works before closing the root session. Glance listens on 8080 internally — keep that bound to localhost.
Install Docker
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 $USERLog out + back in, then verify with docker run --rm hello-world.
Deploy with Docker Compose
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.ymlservices:
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_Yorkdocker compose up -d
docker compose logs -f
curl -I http://127.0.0.1:8080 # should return 200127.0.0.1:8080:8080 binds Glance to loopback only — NGINX reaches it fine, external scanners cannot.
Build a Sensible Configuration
The default glance.yml is a feature demo with noisy external API hits. Replace with a leaner config:
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: VPSproxied: 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.
echo "GITHUB_TOKEN=ghp_yourtokenhere" > /opt/glance/.env
chmod 600 /opt/glance/.envThen propagate it in compose env:
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.
NGINX Reverse Proxy + TLS
sudo apt install -y nginx certbot python3-certbot-nginxserver {
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;
}
}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 -nAdd Authentication
Glance is now public. Either accept it (RSS, public releases, weather are non-sensitive) or enable built-in auth.
docker run --rm glanceapp/glance secret:make
docker run --rm glanceapp/glance password:hash 'your-strong-password-here'auth:
secret-key: <paste secret here>
users:
admin:
password-hash: <paste hash here>docker compose restartPer-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.
Updates
cd /opt/glance
docker compose pull
docker compose up -dFor production, pin to a tag like glanceapp/glance:v0.7.0 instead of :latest.
Backups
#!/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 -deletesudo chmod +x /etc/cron.daily/glance-backupNo 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.
Alternative: Native Binary with systemd
Skip Docker entirely on very small VPS plans where the daemon's overhead is unwelcome.
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/glanceDrop your glance.yml directly in /opt/glance/glance.yml (the binary looks for it beside itself, not in a subdir).
[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.targetsudo systemctl daemon-reload
sudo systemctl enable --now glance
sudo systemctl status glanceThe 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-author 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: truemissing from theserverblock — without forwarded headers, the cookie gets rejected as non-HTTPS. - Subpath assets 404: need both
base-url: /glancein the config and a proxy that strips the prefix (proxy_pass http://127.0.0.1:8080/;with trailing slash).
