Introduction: real-world use case
Grafana is the industry-standard open-source platform for operational dashboards, alerting, and unified observability. Whether you are visualising Prometheus metrics from Kubernetes, querying logs from Loki, or stitching together business KPIs from PostgreSQL and InfluxDB, Grafana sits at the centre of the stack.
A common pain point in self-hosted deployments is routing Grafana safely behind a reverse proxy that handles HTTPS, automatic Let's Encrypt certificates, and clean path-based or host-based routing without manual certificate renewal. Traefik v3 solves exactly this — it discovers containers automatically via Docker labels, provisions TLS certificates from Let's Encrypt, and provides a dashboard to inspect live routing rules.
This guide walks you through a production-ready deployment of Grafana 11 alongside Traefik v3 on a single Ubuntu 22.04/24.04 server. By the end you will have Grafana served at https://grafana.example.com with automatic TLS, persistent storage, a secured Traefik dashboard, and a Prometheus data source pre-wired for node metrics.
Architecture/flow overview
The deployment uses three containers managed by a single Docker Compose file:
- Traefik v3 — Listens on ports 80 and 443. Terminates TLS, issues Let's Encrypt certificates, and proxies requests to backend containers by reading Docker labels. Exposes its own dashboard at
https://traefik.example.combehind Basic Auth. - Grafana 11 — Serves the UI and API at
https://grafana.example.com. Persists data (dashboards, users, data-source configs) in a named Docker volume. Communicates with data sources over the internal Docker network. - Prometheus — Optional but included for a complete observability baseline. Scrapes node-exporter and Grafana's own metrics endpoint. Grafana auto-provisions it as a data source via a provisioning config file.
Traffic flow: Browser → :443 → Traefik → grafana:3000. Let's Encrypt ACME challenges are served by Traefik on port 80 (HTTP-01 challenge). All internal service communication happens on a private Docker bridge network named proxy.
Prerequisites
- Ubuntu 22.04 or 24.04 server with a public IP address
- DNS A records:
grafana.example.comandtraefik.example.compointing to your server IP - Ports 80 and 443 open in your firewall/security group
- Docker Engine 24+ and Docker Compose v2 installed
- A valid email address for Let's Encrypt registration
- Root or sudo access
Step-by-step deployment
Step 1 — Install Docker and Docker Compose
If Docker is not yet installed, run the official convenience script:
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
newgrp docker
docker --version
docker compose version
Manual copy: select the text above and paste into your terminal.
Step 2 — Create project directory and shared proxy network
Traefik and all proxied services must share a Docker network so Traefik can reach their containers.
mkdir -p ~/grafana-traefik/{traefik,grafana/provisioning/datasources}
cd ~/grafana-traefik
docker network create proxy 2>/dev/null || true
Manual copy: select the text above and paste into your terminal.
Step 3 — Create the Traefik static configuration
Traefik reads its main configuration from traefik/traefik.yml. This file defines entry points, the Let's Encrypt resolver, and enables the Docker provider.
cat > traefik/traefik.yml <<'EOF'
global:
checkNewVersion: false
sendAnonymousUsage: false
api:
dashboard: true
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
certificatesResolvers:
letsencrypt:
acme:
email: [email protected]
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
providers:
docker:
exposedByDefault: false
network: proxy
EOF
Manual copy: replace [email protected] with your real email before running.
Step 4 — Create the environment file
Store sensitive values in a .env file that Docker Compose will load automatically. Never commit this file to version control.
cat > .env <<'EOF'
GRAFANA_DOMAIN=grafana.example.com
TRAEFIK_DOMAIN=traefik.example.com
TRAEFIK_DASHBOARD_AUTH=admin:$$apr1$$... # generate with: htpasswd -nb admin yourpassword
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=ChangeMe123!
GF_SERVER_ROOT_URL=https://grafana.example.com
EOF
Generate the htpasswd hash for the Traefik dashboard:
sudo apt-get install -y apache2-utils
htpasswd -nb admin 'YourSecurePassword'
# Copy the output (e.g. admin:$apr1$...) and escape every $ as $$ in .env
Manual copy: replace domain names and passwords before running.
Step 5 — Write the Docker Compose file
cat > docker-compose.yml <<'EOF'
version: "3.9"
networks:
proxy:
external: true
volumes:
letsencrypt:
grafana_data:
prometheus_data:
services:
traefik:
image: traefik:v3.0
restart: unless-stopped
networks:
- proxy
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik/traefik.yml:/traefik.yml:ro
- letsencrypt:/letsencrypt
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`${TRAEFIK_DOMAIN}`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=${TRAEFIK_DASHBOARD_AUTH}"
prometheus:
image: prom/prometheus:latest
restart: unless-stopped
networks:
- proxy
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.retention.time=15d"
- "--web.enable-lifecycle"
grafana:
image: grafana/grafana-oss:11.0.0
restart: unless-stopped
networks:
- proxy
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
environment:
- GF_SECURITY_ADMIN_USER=${GF_SECURITY_ADMIN_USER}
- GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD}
- GF_SERVER_ROOT_URL=${GF_SERVER_ROOT_URL}
- GF_USERS_ALLOW_SIGN_UP=false
- GF_AUTH_ANONYMOUS_ENABLED=false
labels:
- "traefik.enable=true"
- "traefik.http.routers.grafana.rule=Host(`${GRAFANA_DOMAIN}`)"
- "traefik.http.routers.grafana.entrypoints=websecure"
- "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
- "traefik.http.services.grafana.loadbalancer.server.port=3000"
EOF
Manual copy: select all text inside the heredoc and paste it into your editor.
Step 6 — Write Prometheus scrape config
cat > prometheus.yml <<'EOF'
global:
scrape_interval: 15s
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]
- job_name: grafana
static_configs:
- targets: ["grafana:3000"]
metrics_path: /metrics
EOF
Manual copy: select the text above and paste into your terminal.
Step 7 — Provision the Prometheus data source in Grafana
cat > grafana/provisioning/datasources/prometheus.yml <<'EOF'
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
EOF
Manual copy: select the text above and paste into your terminal.
Step 8 — Create the ACME storage file with correct permissions
mkdir -p traefik
# Traefik requires acme.json to have 600 permissions on the host
# For Docker volumes, permissions are managed inside the container
# Ensure the volume is created before first run
docker volume create letsencrypt 2>/dev/null || true
Manual copy: select the text above and paste into your terminal.
Step 9 — Start all services
docker compose up -d
docker compose ps
docker compose logs -f traefik
Manual copy: select the text above and paste into your terminal.
Wait 30–60 seconds for Traefik to acquire Let's Encrypt certificates, then open https://grafana.example.com in your browser.
Configuration and secrets handling
Never store secrets in docker-compose.yml. Always use the .env file or Docker secrets. Key practices for this stack:
- Add
.envto your.gitignoreimmediately:echo ".env" >> .gitignore - Rotate
GF_SECURITY_ADMIN_PASSWORDafter first login via Grafana's Profile page. - Enable SMTP for alerting by adding
GF_SMTP_*variables to.envand passing them as environment vars in thegrafana:service block. - For production, consider mounting a
grafana.inioverride file instead of using individual environment variables — it is easier to audit. - Restrict Docker socket access: create a dedicated Traefik user and use a Docker socket proxy (e.g.,
tecnativa/docker-socket-proxy) to limit label-reading without full daemon access. - Rotate Let's Encrypt certificates automatically — Traefik handles this, but verify renewal is working after 60 days by checking
traefik.logfor ACME renewal messages.
Verification checklist
- ☑
docker compose psshows all three containers asUp - ☑
https://grafana.example.comloads the Grafana login page with a valid TLS certificate - ☑ Login with
admin/ your password succeeds - ☑ Navigate to Connections → Data sources — Prometheus appears as default and "Save & Test" returns green
- ☑
https://traefik.example.comloads the Traefik dashboard after Basic Auth prompt - ☑ Traefik dashboard shows two routers (
traefik@docker,grafana@docker) as healthy - ☑ HTTP-to-HTTPS redirect works:
curl -I http://grafana.example.comreturns301 - ☑
docker compose logs grafana | grep "HTTP Server Listen"shows no startup errors
# Quick verification one-liner
curl -sI https://grafana.example.com | head -5
curl -sI https://traefik.example.com | head -5
docker compose exec grafana grafana-cli admin reset-admin-password --help # confirm CLI access
Manual copy: select the text above and paste into your terminal.
Common issues/fixes
- Certificate not issued (Traefik logs show ACME errors): Ensure port 80 is open and DNS resolves to your server IP. Let's Encrypt HTTP-01 challenge requires port 80 reachable from the internet. Check
docker compose logs traefik | grep -i acme. - Grafana returns 502 Bad Gateway: The Grafana container is still starting. Wait 10–15 seconds and reload. Check
docker compose logs grafanafor database migration messages — these can take time on first boot. - Traefik dashboard shows router as "red": The
TRAEFIK_DOMAINDNS entry is not resolving. Rundig traefik.example.comand ensure it returns your server IP. - Permission denied on grafana_data volume: Grafana runs as UID 472. If you mount a host directory, run
sudo chown -R 472:472 ./grafana-databefore starting containers. With named volumes this is handled automatically. - Prometheus data source test fails: Verify Prometheus is running (
docker compose ps prometheus) and thatgrafanaandprometheusare on the sameproxynetwork (docker network inspect proxy). - Admin password forgotten: Reset with
docker compose exec grafana grafana-cli admin reset-admin-password newpassword. - Too many redirects: Usually caused by a misconfigured
GF_SERVER_ROOT_URL. Ensure it includes the full HTTPS URL with no trailing slash and matches your domain exactly.
FAQ
Can I add more data sources after deployment?
Yes. Add additional YAML files to grafana/provisioning/datasources/ following the same format as prometheus.yml, then run docker compose restart grafana. Grafana will pick up the new sources automatically. You can also add data sources via the UI without any restart.
How do I import pre-built dashboards from Grafana.com?
In the Grafana UI, go to Dashboards → Import and enter a dashboard ID from grafana.com/grafana/dashboards. For example, ID 1860 is the Node Exporter Full dashboard. You will need a node-exporter instance and a matching Prometheus job to populate it.
Is it safe to expose the Docker socket to Traefik?
Mounting the Docker socket gives Traefik full daemon access, which is a security risk. For production, replace it with a socket proxy container (tecnativa/docker-socket-proxy) that exposes only the /containers API endpoint. Update the Traefik Docker provider endpoint to tcp://socketproxy:2375.
How do I upgrade Grafana to a newer version?
Update the image tag in docker-compose.yml (e.g., grafana/grafana-oss:11.1.0), then run docker compose pull grafana && docker compose up -d grafana. Grafana applies database migrations automatically on first start. Always back up the grafana_data volume before upgrading.
How do I set up alerting in Grafana?
Go to Alerting → Contact points and add a contact point (email, Slack, PagerDuty, etc.). Enable SMTP by adding the following to your .env file and restarting Grafana:
GF_SMTP_ENABLED=true
GF_SMTP_HOST=smtp.yourprovider.com:587
[email protected]
GF_SMTP_PASSWORD=yoursmtppassword
[email protected]
Manual copy: add these lines to your .env file.
Can I run this behind Cloudflare instead of direct Let's Encrypt?
Yes. Set Cloudflare as your DNS proxy and switch Traefik to the DNS-01 ACME challenge using the cloudflare provider. Add your Cloudflare API token as an environment variable in the Traefik service and update traefik.yml to use dnsChallenge instead of httpChallenge. This also enables wildcard certificates.
How do I back up my Grafana dashboards and data?
The easiest approach is to back up the Docker volume:
docker run --rm -v grafana-traefik_grafana_data:/data \
-v $(pwd)/backups:/backup alpine \
tar czf /backup/grafana_data_$(date +%Y%m%d).tar.gz -C /data .
Alternatively, use Grafana's HTTP API to export dashboards as JSON and store them in a Git repository alongside your provisioning configs for GitOps-style management. Manual copy: select the text above and paste into your terminal.
Internal links
- Production Guide: Deploy Authelia with Docker Compose and Traefik ForwardAuth on Ubuntu
- Deploy Nextcloud with Docker Compose, Nginx, and Redis on Ubuntu (Production Guide)
- Production Guide: Deploy Plausible with Docker Compose + Caddy + ClickHouse on Ubuntu
Talk to us
Need help deploying a production-ready Grafana observability stack, integrating it with your existing Prometheus or Loki setup, or designing alerting runbooks for your infrastructure? We can help with architecture, secure rollout, data source integration, and dashboard design.