Every production system generates signals: CPU spikes, error logs, request latencies, disk pressure. Without a central place to see them, you are flying blind. Grafana is the open-source visualization layer that turns raw metrics and logs into dashboards you can actually use. Pair it with Prometheus for metrics and Loki for logs, and you have a self-hosted observability stack that rivals paid services at zero recurring cost.
This guide covers a complete self-host Grafana deployment using Docker Compose. You will collect system metrics, aggregate application logs, and build your first dashboard. For alternative setups, see our complete developer guide or the expanded monitoring deep dive.
What You Need Before Starting
You do not need a Kubernetes cluster or a dedicated observability team. A single VPS handles small to medium workloads:
- A Linux server with at least 2 vCPUs, 4 GB RAM, and 30 GB SSD
- Docker Engine 24.x+ and Docker Compose v2 installed
- A domain or subdomain for Grafana (optional but recommended for TLS)
- Basic familiarity with YAML, Prometheus query language, and Linux
Plan for storage growth. Metrics and logs accumulate fast. A 30-day retention policy with 10 containers can consume 10 GB per month.
Project Structure and Docker Compose
Create a project directory and pull the official images. We will run Grafana, Prometheus, Loki, Promtail, and Node Exporter as a single Compose stack:
sudo mkdir -p /opt/monitoring
sudo chown $USER:$USER /opt/monitoring
cd /opt/monitoring
mkdir -p {prometheus,loki,grafana/provisioning/datasources}
Create the docker-compose.yml:
services:
prometheus:
image: prom/prometheus:v2.53.0
container_name: prometheus
restart: unless-stopped
volumes:
- ./prometheus:/etc/prometheus
- prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.enable-lifecycle'
networks:
- monitoring
loki:
image: grafana/loki:3.0.0
container_name: loki
restart: unless-stopped
volumes:
- ./loki:/etc/loki
ports:
- '127.0.0.1:3100:3100'
command: -config.file=/etc/loki/loki-config.yml
networks:
- monitoring
promtail:
image: grafana/promtail:3.0.0
container_name: promtail
restart: unless-stopped
volumes:
- /var/log:/var/log:ro
- ./loki:/etc/loki
command: -config.file=/etc/loki/promtail-config.yml
networks:
- monitoring
grafana:
image: grafana/grafana:11.0.0
container_name: grafana
restart: unless-stopped
ports:
- '127.0.0.1:3000:3000'
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
networks:
- monitoring
node-exporter:
image: prom/node-exporter:v1.8.0
container_name: node-exporter
restart: unless-stopped
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--path.rootfs=/rootfs'
networks:
- monitoring
volumes:
prometheus-data:
grafana-data:
networks:
monitoring:
driver: bridge
We bind Grafana and Loki to localhost so they are only reachable through a reverse proxy. Prometheus and Node Exporter have no exposed ports at all — they communicate internally.
Configuring Prometheus and Loki
Create the Prometheus scrape configuration:
# prometheus/prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
Create a minimal Loki configuration:
# loki/loki-config.yml
auth_enabled: false
server:
http_listen_port: 3100
ingester:
lifecycler:
address: 127.0.0.1
ring:
kvstore:
store: inmemory
replication_factor: 1
chunk_idle_period: 5m
chunk_retain_period: 30s
schema_config:
configs:
- from: 2020-05-15
store: boltdb
object_store: filesystem
schema: v11
index:
prefix: index_
period: 168h
storage_config:
boltdb:
directory: /tmp/loki/index
filesystem:
directory: /tmp/loki/chunks
Create the Promtail configuration to ship /var/log to Loki:
# loki/promtail-config.yml
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: system-logs
static_configs:
- targets: [localhost]
labels:
job: varlogs
__path__: /var/log/**/*.log
Provisioning Grafana Datasources
Grafana can auto-configure datasources on startup. Create a provisioning file so Prometheus and Loki are ready immediately:
# grafana/provisioning/datasources/datasources.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
- name: Loki
type: loki
access: proxy
url: http://loki:3100
Starting the Stack and First Dashboard
Launch everything:
docker compose up -d
docker compose ps
Verify Prometheus is scraping targets:
curl -s http://127.0.0.1:9090/api/v1/targets | jq '.data.activeTargets[] | {job, health}'
Open Grafana at https://grafana.example.com (or http://127.0.0.1:3000 if testing locally). The default credentials are admin / admin. Change the password immediately.
Import the official Node Exporter Full dashboard (ID: 1860) from the Grafana dashboards repository. It gives you CPU, memory, disk, and network panels without writing a single query.
Log Exploration with Loki
Switch to the Explore view in Grafana, select the Loki datasource, and run a simple query:
{job="varlogs"} |= "error"
This filters all system logs for lines containing "error". Combine with Grafana's log volume histogram to spot error spikes visually. For structured application logs, add labels in Promtail's relabel_configs to filter by service name or severity.
Tips and Troubleshooting
No metrics appearing in Grafana
Check that Prometheus can reach its targets. The expression browser at /graph on port 9090 shows scrape status. If Node Exporter is down, verify the container is on the monitoring network.
Loki returns 500 errors
The filesystem storage backend in this guide is for single-node setups. For production, switch to S3, GCS, or a shared filesystem. Also ensure the /tmp/loki paths inside the container are writable.
Promtail is not shipping logs
Confirm the __path__ glob matches your actual log files. Promtail does not follow symlinks by default. If your logs live in /var/log/journal, use the journald scrape config instead of file targets.
Grafana password reset
If you forget the admin password, reset it from the container:
docker exec -it grafana grafana-cli admin reset-admin-password newpassword
Dashboard import fails
Some community dashboards require specific exporter versions or additional metrics. Check the dashboard documentation for required collectors. The Node Exporter Full dashboard works with the default flags shown above.
High memory usage
Prometheus memory scales with the number of active time series. Reduce cardinality by dropping unnecessary labels, or increase the scrape interval. Loki memory spikes during heavy log ingestion — add rate limits in Promtail if needed.
Next Steps
You now have a self-hosted observability stack: Prometheus collecting metrics, Loki aggregating logs, and Grafana visualizing both. This foundation scales from a single VPS to a multi-node cluster by swapping storage backends and adding remote-write endpoints.
For more detailed guidance, explore our related posts:
- How to Self-Host Grafana: The Complete Developer Guide to Monitoring Dashboards
- How to Self-Host Grafana: The Complete Developer Guide to Monitoring Dashboards
- See Everything: How to Self-Host Grafana for Production Monitoring
Need help designing SLO dashboards, setting up alerting, or scaling your observability stack to multiple clusters? Contact our team for enterprise Grafana consulting and managed monitoring infrastructure.