Skip to Content

Deploy Grafana with Docker Compose and Traefik on Ubuntu: Production-Ready Observability Guide

A step-by-step production guide to running Grafana 11 behind Traefik v3 with automatic TLS, Prometheus data source, and secure dashboard routing.

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.com behind 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.com and traefik.example.com pointing 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 .env to your .gitignore immediately: echo ".env" >> .gitignore
  • Rotate GF_SECURITY_ADMIN_PASSWORD after first login via Grafana's Profile page.
  • Enable SMTP for alerting by adding GF_SMTP_* variables to .env and passing them as environment vars in the grafana: service block.
  • For production, consider mounting a grafana.ini override 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.log for ACME renewal messages.

Verification checklist

  • docker compose ps shows all three containers as Up
  • https://grafana.example.com loads 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.com loads 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.com returns 301
  • 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 grafana for database migration messages — these can take time on first boot.
  • Traefik dashboard shows router as "red": The TRAEFIK_DOMAIN DNS entry is not resolving. Run dig traefik.example.com and 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-data before starting containers. With named volumes this is handled automatically.
  • Prometheus data source test fails: Verify Prometheus is running (docker compose ps prometheus) and that grafana and prometheus are on the same proxy network (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

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.

Contact Us

Deploy Nextcloud with Docker Compose, Nginx, and Redis on Ubuntu (Production Guide)
A production-ready self-hosted file collaboration stack with PostgreSQL, Redis caching, Nginx reverse proxy, TLS, and full day-2 operations guidance.