Docker: Use the container traefik/whoami to troubleshoot nginx headers

Introduction

Today I was fighting quite a bit with the integration of a software called Teslamate with Grafana through nginx reverse proxy, that Christer recommended to me. The issue I had was how to get Grafana to play nicely in a sub-path like https://example.com/grafana/

There are many explanations online on how to do this, and most of them are old and outdated. What is important is to pass on the proper headers from nginx and to configure grafana properly to accept being served under a sub path. This posed some issues for me as I use our “standard stack” of haproxy ---haproxy-protocol---> nginx -> teslamate/grafana.

As a quick reminder, haproxy integrates with nginx over something called “haproxy protocol”, which in principle just pads 150 bytes to the beginning of whatever protocol it connects with (for example http/https) to pass on some metadata. We do this for https on port 444.

This in turn made it a bit tricky to see where in the chain an issue would be. In my case I had to properly forward some X-headers, and I did not know how to easily see what I “actually” set in the nginx config.

Enter the simple container “whoami” from Traefik.

I shut down the teslamate, started whoami on the same port as Grafana, then used curl:

  • Config for whoami
ops@wue-docker-l02:~/docker/teslamate$ cat docker-compose.test.yml 
services:
  whoami:
    ports:
      - 3002:80 #<--- using the port I proxy grafana to
    image: "traefik/whoami"
  • Shut down Teslamate and Grafana
ops@wue-docker-l02:~/docker/teslamate$ docker compose down
[+] Running 1/1
 ✔ Network teslamate_default  Removed                                                                                                                                                         0.3s 
  • Starting whoami
ops@wue-docker-l02:~/docker/teslamate$ docker compose -f docker-compose.test.yml up 
[+] Running 2/2
 ✔ Network teslamate_default     Created                                                                                                                                                      0.1s 
 ✔ Container teslamate-whoami-1  Created                                                                                                                                                      0.1s 
Attaching to whoami-1
whoami-1  | 2024/10/19 13:26:03 Starting up on port 80
  • In a different terminal on my laptop:
15:25 $ curl https://teslamate.example.com/grafana/
Hostname: 8c50a13f857c
IP: 127.0.0.1
IP: ::1
IP: 192.168.25.2
RemoteAddr: 192.168.9.22:34456
GET /grafana/ HTTP/1.1
Host: teslamate.example.com
User-Agent: curl/8.7.1
Accept: */*
Connection: close
X-Forwarded-For: 192.168.4.234
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Real-Ip: 192.168.4.234

And there it is, the headers that I was not sure were correct! Now I could edit the nginx config and figure out the proper configuration in my setting.

You can also see that haproxy and nginx are correctly configured. If I, for example, remove the set_real_ip_from clause in the nginx config for my application:

#set_real_ip_from 192.168.9.0/24;

Then, curl says the following for X-Forwarded-For (classic misconfiguration in a haproxy environment):

X-Forwarded-For: 192.168.9.21 # <--- the haproxy server ip

Instead of the proper:

X-Forwarded-For: 192.168.4.234 # <--- my laptop's ip

Extra credit

Outside the scope of using traefik/whoami, the magic trick to get grafana to work the way I wanted, is to:

  • Configure nginx properly
  • Configure grafana in the teslamate docker-compose.yml file properly

nginx config:

upstream teslamate.example.com {
  server 192.168.4.80:4000 fail_timeout=0;
}

#--- extra config from playbook pre
upstream grafana {
  server 192.168.4.80:3002;
}

server {
  listen 192.168.9.22:444 ssl proxy_protocol;
...
  proxy_set_header        X-Real-IP $remote_addr;
  proxy_set_header        Client-IP $remote_addr;
  proxy_set_header        Host $host;
  proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header        X-Forwarded-Proto $scheme;
  proxy_set_header        X-Original-URI $request_uri;

#--- only needed for haproxy
set_real_ip_from 192.168.9.0/24;
real_ip_header proxy_protocol;

location = /grafana {
  return 301 $scheme://$host/grafana/;
}

location /grafana/ {
  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_set_header Host $host;
  proxy_set_header X-Forwarded-Port 443;
  proxy_pass http://grafana;
}
# Proxy Grafana Live WebSocket connections.
location /grafana/api/live/ {
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Port 443;
  proxy_pass http://grafana;
}
...
}

The grafana part of teslamate/docker-comopse.yml. Anything you set as environment variables for grafana with the prefix GF_ will be considered as parts of the grafana.ini file. For example, GF_SERVER_SERVE_FROM_SUB_PATH=truw will expand to serve_from_sub_path = true in the [server] section of grafana.ini:

  grafana:
    image: teslamate/grafana:latest
    restart: always
    environment:
      - DATABASE_HOST=${DATABASE_HOST:-database}
      - DATABASE_NAME=${DATABASE_NAME:-teslamate}
      - DATABASE_USER=${DATABASE_USER:-teslamate}
      - DATABASE_PASS=${DATABASE_PASS:-teslamate}
      - GF_SERVER_DOMAIN=${FQDN_TM}
      #--- default
      #- GF_SERVER_ROOT_URL=%(protocol)s://%(domain)s:%(http_port)s/grafana
      #--- hard code https for redirection, but protocol remains http between nginx and grafana. (note: one can use https between nginx and grafana also)
      - GF_SERVER_ROOT_URL=https://%(domain)s/grafana/
      - GF_SERVER_SERVE_FROM_SUB_PATH=true
      #--- 
      - GF_SERVER_PROTOCOL=http
      #- GF_LOG_LEVEL=debug

    ports:
      - "${GRAFANA_HTTP_PORT:-3000}:3000"
    volumes:
      - ./data/teslamate-grafana-data:/var/lib/grafana

The .env file:

TESLAMATE_HTTP_PORT=4000
GRAFANA_HTTP_PORT=3002
FQDN_TM=teslamate.example.com
TM_TZ=Europe/Zurich

DATABASE_HOST=database
DATABASE_NAME=teslamate
DATABASE_USER=teslamate
DATABASE_PASS=supersecretpassword

References