
Grafana is what you open when you’re done guessing. CPU spikes, slow queries, 5xx bursts, full disks—Grafana turns those signals into charts you can investigate.
If you’d rather keep metrics and dashboards off a SaaS offering (cost, privacy, compliance, or control), you can deploy Grafana on a VPS in an afternoon.
Treat it like a real service from day one: persistent storage, HTTPS, backups, and updates you schedule.
This guide sticks to an operator-friendly setup. You’ll run Grafana in Docker with durable storage, an HTTPS reverse proxy, a database choice that won’t hurt later, and a backup/upgrade routine you can trust.
This isn’t a lab demo. It’s meant to still feel sane after months of dashboards, alerts, and plugin tweaks.
Why deploy Grafana on a VPS instead of a hosted dashboard service?
Hosted Grafana removes server work. In exchange, you give up a few things: recurring cost that scales with users and data sources, tighter limits around plugins and policy, and fuzzier boundaries around access logs and audit needs.
If you already run a VPS for your apps, self-hosting Grafana is often cheaper than a mid-tier SaaS plan.
- Data locality: keep internal metrics on infrastructure you control (handy for regulated environments or private networks).
- Network proximity: put Grafana near Prometheus, Loki, PostgreSQL, or MySQL to cut latency and avoid egress fees.
- Predictable ops: you decide backup cadence, retention, and when updates land.
If you want stable Linux infrastructure without overthinking the platform layer, a HostMyCode VPS is a straightforward fit.
If you’d rather not be on the hook for base OS maintenance and patching, managed VPS hosting keeps the same flexibility while shifting routine server chores off your plate.
Grafana licensing and what you actually get
Grafana’s open-source edition (Grafana OSS) ships under the AGPLv3 license. That’s usually a non-issue for internal self-hosting.
It matters if you modify Grafana and provide it as a network service to others.
For most organizations running dashboards for themselves, OSS is the right starting point.
Grafana OSS includes what operators typically care about: dashboards, alerting, teams/roles, data sources, plugins, provisioning-as-code, and the HTTP API.
Enterprise features exist, but you can run a serious monitoring setup without them.
What to run Grafana on: VPS sizing that won’t feel tight
Grafana isn’t resource-hungry on day one. Pressure shows up later with more users, more data sources, more alert rules, and more plugins.
Size it with headroom so it stays responsive.
- Small setup (solo operator, a few dashboards): 1 vCPU, 1–2 GB RAM, 20+ GB SSD.
- Team setup (5–20 users, alerting, multiple data sources): 2 vCPU, 4 GB RAM, 40+ GB SSD.
- Busy ops hub (many dashboards, lots of auth/users, heavier plugins): 4 vCPU, 8 GB RAM, 80+ GB SSD.
Performance depends mostly on what Grafana queries. Expensive Prometheus range queries or Loki searches usually stress Prometheus/Loki first, not Grafana.
Still, give Grafana enough RAM to avoid swapping while rendering and caching.
One practical rule: if Grafana shares a VPS with your database or log store, it will get slow at exactly the wrong time.
If this is production monitoring, separate Grafana and your data stores when you can.
Deployment shape: Docker Compose with persistent storage
Grafana officially supports Docker and behaves well under Compose. Reliability comes down to two basics:
- Persistent volumes for
/var/lib/grafana(dashboards, users, alert state, plugin files). - A real database if you expect multiple admins/users, want HA later, or plan to keep the instance for years.
Grafana defaults to SQLite. For a tiny instance, that’s fine.
SQLite is a single file on disk. Backups must be consistent, and concurrency is limited.
For teams, PostgreSQL or MySQL/MariaDB is usually the calmer long-term path.
Recommended directory layout
/opt/grafana/(Compose project)/opt/grafana/data/(Grafana persistent data)/opt/grafana/db/(optional: database volume)/opt/grafana/backups/(local backup staging)
Docker Compose example (Grafana + PostgreSQL)
This is a solid “one VPS” setup for Grafana plus its database. If the instance grows, move PostgreSQL to a dedicated database host.
mkdir -p /opt/grafana/{data,db,backups}
cd /opt/grafana
# Create a minimal env file for secrets
cat > .env <<'EOF'
GRAFANA_DOMAIN=grafana.example.com
POSTGRES_DB=grafana
POSTGRES_USER=grafana
POSTGRES_PASSWORD=change-me-long-random
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=change-me-long-random
EOF
cat > docker-compose.yml <<'EOF'
services:
postgres:
image: postgres:16
container_name: grafana-postgres
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- ./db:/var/lib/postgresql/data
restart: unless-stopped
grafana:
image: grafana/grafana-oss:latest
container_name: grafana
depends_on:
- postgres
environment:
GF_SERVER_DOMAIN: ${GRAFANA_DOMAIN}
GF_SERVER_ROOT_URL: https://${GRAFANA_DOMAIN}
GF_DATABASE_TYPE: postgres
GF_DATABASE_HOST: postgres:5432
GF_DATABASE_NAME: ${POSTGRES_DB}
GF_DATABASE_USER: ${POSTGRES_USER}
GF_DATABASE_PASSWORD: ${POSTGRES_PASSWORD}
GF_SECURITY_ADMIN_USER: ${GF_SECURITY_ADMIN_USER}
GF_SECURITY_ADMIN_PASSWORD: ${GF_SECURITY_ADMIN_PASSWORD}
GF_USERS_ALLOW_SIGN_UP: "false"
volumes:
- ./data:/var/lib/grafana
ports:
- "127.0.0.1:3000:3000"
restart: unless-stopped
EOF
docker compose up -d
Two choices here are intentional:
- Bind Grafana only to localhost (
127.0.0.1:3000). Let the reverse proxy handle the public edge. - Set
GF_SERVER_ROOT_URLto your domain so OAuth callbacks and links stay consistent.
If you’re still building out basic VPS monitoring, HostMyCode has a practical walkthrough for collecting live server metrics with Netdata.
It’s a helpful precursor to Grafana dashboards: Run Netdata on a VPS for live server monitoring.
Reverse proxy + HTTPS: Nginx with Let’s Encrypt
Grafana can terminate TLS itself. In production, most teams put one reverse proxy in front of multiple apps.
Nginx is a common choice. It’s also easy to lock down with reasonable defaults.
Assumptions:
- Ubuntu or Debian VPS
- A domain (A/AAAA record pointing to your VPS)
- Ports 80/443 open
Nginx server block
Create /etc/nginx/sites-available/grafana:
server {
listen 80;
server_name grafana.example.com;
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name grafana.example.com;
ssl_certificate /etc/letsencrypt/live/grafana.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/grafana.example.com/privkey.pem;
# Sensible TLS defaults (keep this file maintained)
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Basic hardening
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
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_pass http://127.0.0.1:3000;
proxy_read_timeout 90s;
}
}
Then enable and reload:
ln -s /etc/nginx/sites-available/grafana /etc/nginx/sites-enabled/grafana
nginx -t && systemctl reload nginx
Certificates
If you already use Certbot, request a certificate for your Grafana subdomain.
If you prefer a simpler automation route, HostMyCode has a Caddy-focused TLS guide with patterns you can reuse: Set up Let’s Encrypt with Caddy for a VPS site.
The core workflow is the same: prove domain ownership, then keep renewals automatic.
Operational check: once TLS is live, make sure Grafana sees the correct scheme.
In Grafana, Configuration → Server Admin → Settings should show your root URL as https://grafana.example.com.
If it doesn’t, fix GF_SERVER_ROOT_URL first, not the proxy.
Database choice: SQLite vs PostgreSQL vs MySQL
Grafana supports SQLite, PostgreSQL, and MySQL/MariaDB. The “right” pick mostly comes down to backup simplicity and how much concurrency you expect.
- SQLite: simplest. Fine for one admin and light usage. Back up carefully so you don’t capture the DB mid-write.
- PostgreSQL: a strong default for teams. Solid concurrency and mature tooling. Easy dumps and restores.
- MySQL/MariaDB: also a good option. If your organization already standardizes on MariaDB, staying consistent can help.
If you already run a production database service, consider placing Grafana’s DB there (or on a dedicated database host). HostMyCode offers database hosting if you don’t want the database living on the same box as the dashboard.
One caution: don’t squeeze Grafana and your only database onto a tiny VPS. Then schedule backups and wonder why dashboards stall.
Backups that actually restore: what to capture and how often
Grafana backups look simple on paper. In practice, consistency and restore testing matter more than the tool you pick.
Minimum backup set
- Grafana data directory:
/opt/grafana/data(plugins, sessions, and local state). - Database: PostgreSQL dump (
pg_dump) or MySQL dump. With SQLite, the DB file lives inside the data directory. - Compose project files:
docker-compose.ymland.env(store secrets securely; don’t leak them into public repos).
Example: nightly PostgreSQL dump + file archive
Create a small backup script, for example /usr/local/sbin/backup-grafana.sh:
#!/usr/bin/env bash
set -euo pipefail
TS=$(date +%F_%H%M)
BASE=/opt/grafana
OUT=${BASE}/backups
mkdir -p "$OUT"
# Dump the Grafana DB from the postgres container
DB_DUMP="$OUT/grafana_pg_${TS}.sql"
docker exec grafana-postgres pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > "$DB_DUMP"
# Archive Grafana data + compose definitions (exclude backups to avoid recursion)
ARCHIVE="$OUT/grafana_files_${TS}.tar.gz"
tar -C "$BASE" \
--exclude='./backups' \
-czf "$ARCHIVE" data docker-compose.yml .env
# Optional: prune old backups (keep 14 days)
find "$OUT" -type f -mtime +14 -name '*.sql' -delete
find "$OUT" -type f -mtime +14 -name '*.tar.gz' -delete
Run it nightly via cron (or a systemd timer). If you use an offsite backup system, push /opt/grafana/backups off the VPS right after each run.
If you want encrypted, offsite retention with tested restore workflows, Borg is a common choice for VPS operators. HostMyCode has a full guide here: BorgBackup setup guide for encrypted offsite backups.
Restore test (don’t skip)
Once a month, do a quick restore drill:
- Spin up a temporary VPS or local Docker environment.
- Restore the PostgreSQL dump into a fresh container.
- Restore
/var/lib/grafana(your/opt/grafana/data). - Confirm you can log in and see dashboards and alert rules.
If you haven’t restored it, it’s not a backup. It’s a file you hope works.
Update workflow: safe upgrades without dashboard downtime surprises
Grafana upgrades are usually uneventful, but it’s still a stateful service. Treat upgrades like any system that stores configuration and identity data.
Plan the change, back up first, and keep a rollback path.
Preflight checklist
- Take a fresh backup (DB + data directory).
- Read Grafana release notes for breaking changes affecting auth, alerting, or plugins.
- Confirm disk space (plugins and migrations can temporarily increase storage use).
Upgrade with Docker Compose
cd /opt/grafana
docker compose pull
docker compose up -d
docker compose logs -f --tail=100 grafana
If the logs show a migration failure, stop and restore.
Repeated restarts rarely “fix” a broken migration. They can also make cleanup harder.
Pin versions once you’re stable
:latest is fine while you’re evaluating. In production, pin a major/minor tag so you don’t jump into a breaking change by accident.
You still upgrade—just on your schedule.
Operational hardening: small settings that prevent big incidents
Grafana often becomes your observability control panel. Treat it like an admin tool, not a public-facing site.
- Disable public sign-ups:
GF_USERS_ALLOW_SIGN_UP=false(shown in the Compose file above). - Put Grafana behind SSO if you already have it. If not, enforce strong passwords and consider 2FA via your auth provider.
- Restrict admin access: keep
/loginreachable, but don’t expose Grafana to the whole internet if it’s staff-only. IP allowlists or a VPN are often enough. - Keep the base OS patched: Grafana isn’t your only attack surface.
If you want a lightweight baseline for Ubuntu patch automation, this HostMyCode guide covers the essentials: Set up automated security updates on Ubuntu VPS.
Common pitfalls (and quick diagnostics)
- “Grafana works on HTTP but breaks on HTTPS”: your
GF_SERVER_ROOT_URLdoesn’t match the public URL, or the proxy isn’t sendingX-Forwarded-Proto. Fix headers and root URL. - Login loop after putting it behind a proxy: cookies are scoped to the wrong domain/scheme. Again, root URL and proxy headers.
- Dashboards load slowly: verify whether the bottleneck is the data source. For Prometheus/Loki, check query timings there first.
- Plugins disappear after restart: you didn’t persist
/var/lib/grafana. Make sure the volume is mapped and writable. - Disk fills up quietly: plugins, image rendering, and logs accumulate. Alert on disk usage. Even a basic uptime monitor helps; see Self-host Uptime Kuma on a VPS with Nginx HTTPS.
Where Grafana fits in a real monitoring stack
Grafana is the view layer. You’ll usually pair it with one or more of the following:
- Prometheus for metrics (node_exporter, app exporters, custom metrics).
- Loki for logs (promtail/agent shipping).
- PostgreSQL/MySQL as direct data sources for business metrics.
- Alerting routed to email, Slack, Teams, or webhooks.
If you’re paying for a hosted APM mainly to get a few “is it up?” dashboards, Grafana plus a minimal metrics stack can cover most day-to-day operator needs.
It also avoids turning observability into an expensive subscription you resent.
Want dashboards without SaaS lock-in? Run Grafana on a HostMyCode VPS and keep monitoring close to your apps and databases. If you want the same control with fewer maintenance tasks, managed VPS hosting is the simplest way to keep a stable, patched base OS.
FAQ
Can I run Grafana on the same VPS as my website or API?
You can, especially for small sites, but understand the trade-off. When your app is under load, Grafana is usually the first thing you reach for.
Separating them avoids the classic failure mode: “monitoring dies when production is on fire.”
Is SQLite OK for Grafana in production?
SQLite works for a single admin and a small number of users. If you expect team usage, frequent dashboard edits, or a long-lived instance, PostgreSQL or MySQL/MariaDB avoids backup and concurrency headaches.
What exactly should I back up for Grafana?
Back up the Grafana data directory (/var/lib/grafana via your mapped volume) and the database you configured (PostgreSQL/MySQL).
Keep your Compose and environment files as well, so rebuilds are repeatable.
How do I update Grafana safely?
Take a fresh backup, pull the new container image, restart with docker compose up -d, then watch logs for migrations.
In production, pin image tags instead of using latest.
Do I need a dedicated server for Grafana?
Most teams don’t. A VPS is plenty for Grafana itself.
You start considering a dedicated server when your data sources (metrics/logs) get heavy, or you’re centralizing monitoring for many servers and services.
Summary: a Grafana setup you can run for years
A good self-hosted Grafana deployment isn’t complicated. But it does need a few deliberate choices.
Use persistent storage, terminate HTTPS at the edge, pick a database that matches your team size, and keep backups you’ve restored at least once.
Then build a quiet, repeatable upgrade routine.
If you want a clean place to run it, start with a HostMyCode VPS and keep Grafana close to the infrastructure it watches.
That’s the real payoff of self-hosting: fewer unknowns, faster feedback, and dashboards that stay under your control.