certbot cannot issue for internal hostnames (*.nas.local), which made nginx-enabled services with internal names fail on deploy. Add a <service>_nginx_letsencrypt flag (default false) and computed <service>_nginx_ssl_certificate/_key paths: when off the vhost uses the shared self-signed cert (nginx_default_ssl_cert), when on it uses the Let's Encrypt lineage. Gate the certbot include on the flag and apply the pattern to all service roles (deluge, fdroid, gitea, grafana, immich, metabase, ntfy, prowlarr, radarr, seerr, static_web, uptime_kuma).
Grafana Role
Installs and configures Grafana as a system service with PostgreSQL backend.
Description
This role:
- Installs Grafana from official repositories (Arch Linux or Debian/Ubuntu)
- Creates a dedicated PostgreSQL database and user
- Configures Grafana to use PostgreSQL
- Optionally configures Nginx reverse proxy
- Optionally provisions disk alerting (temperature + capacity) to ntfy (see Alerting)
- Follows the shared services pattern
Dependencies
postgresrole (for database backend)
Variables
Required Variables (Override with Ansible Vault!)
grafana_db_password: changeme
grafana_admin_password: changeme
grafana_secret_key: changeme
Server Configuration
grafana_http_port: 3000
grafana_domain: localhost
grafana_root_url: "%(protocol)s://%(domain)s:%(http_port)s/"
Database Configuration
grafana_db_type: postgres
grafana_db_name: grafana
grafana_db_user: grafana_user
grafana_db_host: localhost
grafana_db_port: 5432
grafana_db_ssl_mode: disable
Admin User
grafana_admin_user: admin
Analytics
grafana_reporting_enabled: false
grafana_check_for_updates: false
Nginx Reverse Proxy
grafana_nginx_enabled: false
grafana_nginx_hostname: grafana.local
Alerting
grafana_alerting_enabled: false
grafana_ntfy_webhook_url: "https://ntfy.example.com/sys-alerts?template=grafana"
grafana_ntfy_token: "{{ vault_ntfy_token }}" # write access to the topic
grafana_ntfy_webhook_url and grafana_ntfy_token are required when
grafana_alerting_enabled: true. The token is rendered into the contact point
provisioning file, which is deployed 0600.
Example Playbook
---
- hosts: monitoring
roles:
- role: grafana
vars:
grafana_domain: monitoring.example.com
grafana_nginx_enabled: true
grafana_nginx_hostname: grafana.example.com
grafana_admin_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
...
grafana_db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
...
grafana_secret_key: !vault |
$ANSIBLE_VAULT;1.1;AES256
...
Architecture
Database Isolation
Grafana gets its own PostgreSQL database with a dedicated user that has minimal privileges:
CREATE DATABASE grafana;
CREATE USER grafana_user WITH PASSWORD 'xxx';
GRANT ALL PRIVILEGES ON DATABASE grafana TO grafana_user;
ALTER USER grafana_user NOSUPERUSER NOCREATEDB NOCREATEROLE;
Service Layout
System Services:
├── PostgreSQL (port 5432, localhost only)
│ └── grafana database → grafana_user
└── Grafana (port 3000, localhost only)
└── Nginx (port 80) → reverse proxy (optional)
Post-Installation
Access Grafana
Direct access:
http://localhost:3000
With Nginx reverse proxy:
http://grafana.local
Default Credentials
- Username:
admin(or value ofgrafana_admin_user) - Password: Value of
grafana_admin_password
Important: Change the admin password immediately after first login!
Verify Installation
# Check service status
systemctl status grafana-server # Debian/Ubuntu
systemctl status grafana # Arch Linux
# Check database connection
sudo -u postgres psql -c "\l" | grep grafana
sudo -u postgres psql -c "\du" | grep grafana
# Check configuration
sudo cat /etc/grafana/grafana.ini | grep -A 5 "\[database\]"
# Check logs
journalctl -u grafana-server -f # Debian/Ubuntu
journalctl -u grafana -f # Arch Linux
Security Considerations
- Passwords: Always use Ansible Vault for sensitive variables
- Database: Grafana user has minimal privileges (no superuser, can't create databases/roles)
- Network: Grafana binds to localhost by default, use Nginx for external access
- Secret Key: Used for signing cookies and other security features - keep it secret!
Nginx Integration
When grafana_nginx_enabled: true, the role:
- Deploys an Nginx vhost configuration to
/etc/nginx/conf.d/grafana.conf - Configures reverse proxy with WebSocket support for live updates
- Reloads Nginx gracefully
When disabled, the vhost configuration is removed.
Alerting
When grafana_alerting_enabled: true, the role provisions Grafana Unified
Alerting as code under
{{ grafana_provisioning_path }}/alerting/:
contact-points.yaml— stock webhook receiver pointing at an ntfy topic, token sent asAuthorization: Bearer. Deployed0600(secret).policies.yaml— notification policy tree (root route to that receiver).rules.yaml— disk alert rules: HDD temperature (smartctl_device_temperature{temperature_type="current"}) and capacity (ZFS poolszfs_pool_capacity_percent; local filesystemsnode_filesystem_avail_bytes/node_filesystem_size_bytes, filtered tofstype=~"ext4|xfs|btrfs").
No plugin, bridge or Alertmanager is involved: the webhook URL carries
?template=grafana, so ntfy's built-in grafana template renders Grafana's
webhook JSON into a 🚨/✅ title and message. Resolved alerts are sent
automatically (disableResolveMessage: false). Adding rules means adding
entries to rules.yaml; the receiver and policy stay generic.
Provisioning the notification policy tree replaces the entire tree; UI edits to policies are overwritten on the next deploy.
Capacity tiers
Cumulative thresholds: a rise fires each newly crossed threshold (lower tiers
keep firing), a fall acks each crossed-down threshold. ZFS capacity is emitted
hourly by the zfs role, so its tiers need at least an hourly for to see the
new value.
| Metric | Threshold | severity |
for |
|---|---|---|---|
zfs_pool_capacity_percent / filesystem used % |
≥80% | warning | 6h |
zfs_pool_capacity_percent / filesystem used % |
≥90% | high | 1h |
zfs_pool_capacity_percent / filesystem used % |
≥95% | critical | 15m |
ZFS datasets are excluded from the filesystem rule: they report pool-wide free space, not per-dataset usage, so the pool metric is authoritative.
OS Support
- Arch Linux: Installs from community repository
- Debian/Ubuntu: Installs from official Grafana APT repository
Troubleshooting
Grafana won't start
# Check logs
journalctl -u grafana-server -n 50 # Debian/Ubuntu
journalctl -u grafana -n 50 # Arch Linux
# Check configuration syntax
sudo grafana-cli admin validate-config
Can't connect to PostgreSQL
# Verify PostgreSQL is running
systemctl status postgresql
# Test database connection
psql -h localhost -U grafana_user -d grafana
# Check PostgreSQL logs
journalctl -u postgresql -n 50
502 Bad Gateway with Nginx
# Verify Grafana is running
systemctl status grafana-server
# Check if Grafana is listening
ss -tlnp | grep 3000
# Test direct access
curl http://localhost:3000