What it is
Grafana is the visualization frontend of the stack.
You log into a web UI, configure one or more datasources and build dashboards made of graphs, gauges, tables and much more. Each of them runs a PromQL query against the datasource and plots the result.
For us:
- Runs as a Docker container on the VPS (in the same compose file as VictoriaMetrics).
- Reaches VictoriaMetrics via the Docker network using the service name
victoriametrics:8428(no port exposure needed on the host). - Exposed publicly at
https://farnetiandrea.it/metrics/via my existing nginx + certbot stack as a reverse-proxy.
1. Setup
1. Create the directory
mkdir -p ~/observability/grafana-data
cd ~/observability2. Set ownership to UID 472
WARNING
Not optional. Grafana inside the container runs as UID 472 (a Grafana-specific user, not 1000 like VictoriaMetrics). Without this chown, the container crashes with messages like
mkdir: cannot create directory '/var/lib/grafana/plugins': Permission denied.
sudo chown -R 472:472 grafana-data3. Pick a free host port
Grafana inside the container listens on 3000, but on the host you can map it to any port you want. First check that the chosen host port is free, because 3000 is one of the most common defaults on Linux servers (Node.js apps, other dashboards, …):
ss -tlnp | grep -E ':(3000|3001)\s'- If you get no output, port
3000is free → use3000in the compose file. - If something is listening on
3000, pick the next free one (3001,3030, whatever). Make sure you adjust both the composeports:line and the nginxproxy_passaccordingly.
For the rest of this page I’ll use 3001 because in my setup 3000 is taken by another app.
4. Add Grafana to the existing docker-compose.yml
Edit ~/observability/docker-compose.yml and add the grafana: service to the existing services: block.
Your full compose file should look like this:
services:
victoriametrics:
image: victoriametrics/victoria-metrics:v1.107.0
container_name: victoriametrics
restart: unless-stopped
user: "1000:1000"
ports:
- "<VPS_PRIVATE_IP>:8428:8428"
volumes:
- ./victoriametrics-data:/storage
command:
- "-storageDataPath=/storage"
- "-retentionPeriod=1"
- "-httpListenAddr=:8428"
grafana:
image: grafana/grafana-oss:11.3.0
container_name: grafana
restart: unless-stopped
user: "472:472"
depends_on:
- victoriametrics
ports:
- "127.0.0.1:3001:3000" # host 3001 → container 3000
volumes:
- ./grafana-data:/var/lib/grafana
environment:
- GF_SERVER_ROOT_URL=https://farnetiandrea.it/metrics/
- GF_SERVER_SERVE_FROM_SUB_PATH=true
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=changeme-please
- GF_USERS_ALLOW_SIGN_UP=false
- GF_USERS_DEFAULT_THEME=dark
networks:
default:
name: observabilityThe points to notice:
127.0.0.1:3001:3000— Grafana listens only on the VPS’s loopback interface. Nginx will proxy to it.GF_SERVER_ROOT_URLandGF_SERVER_SERVE_FROM_SUB_PATH=true— together, they tell Grafana “you live at the path/metrics, generate all internal URLs and redirects accordingly”. Without these, the login form, API calls and static assets all break.GF_SECURITY_ADMIN_PASSWORDis a placeholder. Change it before starting, or change it via web UI on first login.
5. Start Grafana
docker compose up -d
docker compose psExpected: both victoriametrics and grafana in running state.
If not, you can check logs as always:
docker compose logs grafana --tail=30Quick test from the VPS itself (Grafana is on localhost:3001):
curl -s http://localhost:3001/api/health
# Expected: {"commit":"...","database":"ok","version":"11.3.0"}2. Nginx configuration
I’m editing the existing nginx server block for farnetiandrea.it:
sudo nano /etc/nginx/sites-available/farnetiandrea.itAnd I add this location directive (alongside the other locations I have):
location /metrics/ {
proxy_pass http://localhost:3001; # NO trailing slash — see note below
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;
# Grafana uses WebSockets for live tail, alerts, etc.
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}IMPORTANT
No trailing slash on
proxy_pass http://localhost:3001. This is the detail that determines whether Grafana works or gets stuck in a redirect loop.
Test the config and reload nginx:
sudo nginx -t
sudo systemctl reload nginxVerification
Open: https://farnetiandrea.it/metrics/
Expected: Grafana login screen.
If so, log in with admin / the password from GF_SECURITY_ADMIN_PASSWORD.
3. Grafana configuration
Configure the VictoriaMetrics datasource
In the Grafana UI:
- Left sidebar → Connections → Data sources → Add data source.
- Search and pick Prometheus (yes, Prometheus: I’ll explain why in a minute).
- Fill in:
- Name:
prometheus(orVictoriaMetrics, whatever you prefer, it’s just a label). - URL:
http://victoriametrics:8428(Docker DNS: both containers are on theobservabilitynetwork, they resolve each other by service name). - Access: leave as
Server (default). - Everything else: defaults.
- Name:
- Click Save & test at the bottom.
Expected: green banner “Successfully queried the Prometheus API”.
INFO
Why “Prometheus” and not the dedicated “VictoriaMetrics” datasource plugin?
VictoriaMetrics is API-compatible with Prometheus: Grafana’s built-in Prometheus connector speaks to it natively, no plugin needed. There exists a separate VictoriaMetrics datasource plugin that adds VM-specific features, but for standard observability with PromQL, the built-in Prometheus datasource is:
- More portable: if you swap DB tomorrow, dashboards probably will keep working.
- The community standard: every pre-made dashboard on
grafana.com/dashboardsexpects thePrometheusdatasource type as a parameter.
Our first query
Left sidebar → Explore → make sure the datasource at the top is the one you configured.
Type:
upAnd click Run query. It should answer.
Great! we’ve finished! Time to build real dashboards now!
If you want to take your first steps, check out the dedicated page.
EXTRA: Anonymous viewer mode
By default Grafana requires a login.
If you want to share your dashboards publicly as a read-only showcase (just like my https://farnetiandrea.it/metrics/, anybody can see them without an account), Grafana has a native anonymous viewer mode.
The anonymous user gets the Viewer role: can browse and zoom into any panel, but cannot edit, delete, change datasources, or access admin pages.
You (the real admin) can still log in via the “Sign in” button in the top-right corner.
You just need to add these four env vars to the grafana: service in your docker-compose.yml:
environment:
# ... your existing vars ...
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_NAME=Main Org.
- GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
- GF_AUTH_ANONYMOUS_HIDE_VERSION=trueAnd to restart Grafana:
docker compose up -d grafanaYou can test in an incognito browser window (no cookies), and you should land directly on the Grafana home dashboard without login prompt.