0 Votes

Dashy

Modifié par john le 2026/09/26 16:54

Tableau de bord d'accueil du serveur principal : liens vers les services auto-hébergés avec leur état, et widgets. Il compte quatre pages : l'accueil (conf.yml) et trois pages secondaires.

Les valeurs propres à l'infrastructure sont remplacées par des marqueurs <ENTRE_CHEVRONS> : <DOMAINE>, adresses IP, comptes, mots de passe et clés.

Déploiement

Stack Portainer

services:
 dashy:
   image: lissy93/dashy:latest
   container_name: dashy
   environment:
      - NODE_ENV=production
      - UID=1000
      - GID=1000
      - NODE_TLS_REJECT_UNAUTHORIZED=0
   ports:
      - "4000:8080"
   volumes:
      - <APPDATA>/Dashy/user-data:/app/user-data
   restart: unless-stopped
   healthcheck:
     test: ['CMD', 'node', '/app/services/healthcheck']
     interval: 90s
     timeout: 10s
     retries: 3
     start_period: 40s
   networks:
      - proxy

networks:
 proxy:
   external: true
  • NODE_TLS_REJECT_UNAUTHORIZED=0 : Dashy accepte les certificats auto-signés des services locaux (vérifications d'état, proxy CORS).
  • user-data contient toute la configuration. Dashy sert ce dossier à la racine du site.
  • Le port 4000 donne un accès direct depuis le LAN, sans SWAG ni Authelia. SWAG n'en a pas besoin : il joint le conteneur par le réseau proxy.

SWAG

<APPDATA>/swag/config/nginx/proxy-confs/30-dashy.subdomain.conf :

server {
   listen 443 ssl;
   listen [::]:443 ssl;
   server_name dash.<DOMAINE>;

   include /config/nginx/ssl.conf;
   include /config/nginx/snippets/robots-default.conf;

   client_max_body_size 25m;

   # Authelia devant tout le domaine
   include /config/nginx/authelia-server.conf;

   # Upstream Dashy
   set $upstream_app  dashy;
   set $upstream_port 8080;
   set $upstream_proto http;

   # (facultatif) proxy vers /pico/
   location /pico/ {
       include /config/nginx/authelia-location.conf;
       proxy_pass http://<IP_PICO>/;
       include /config/nginx/proxy.conf;
       proxy_set_header Connection close;
    }

   # API de Dashy (sauvegarde de la config, vérifications d'état)
   location ^~ /api/ {
       include /config/nginx/authelia-location.conf;
       include /config/nginx/proxy.conf;
       include /config/nginx/resolver.conf;
       proxy_pass $upstream_proto://$upstream_app:$upstream_port;
    }
   location = /cors-proxy {
       include /config/nginx/authelia-location.conf;
   # Dashy envoie la cible dans ce header
   set $target $http_target_url;
   if ($target = "") { return 400; }

   add_header Access-Control-Allow-Origin "*" always;
   add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
   add_header Access-Control-Allow-Headers "*" always;
   if ($request_method = OPTIONS) { return 204; }

   # TLS (certs auto-signés possibles sur les cibles LAN, ex: Nextcloud)
   proxy_ssl_server_name on;
   proxy_ssl_verify off;

   # Par défaut, on ne fait pas confiance à l'input
   set $auth_header "";
   set $accept_header "";
   set $ocs_header "";
   set $host_override "";

   access_by_lua_block {
     local target = ngx.var.target

     local host, port = target:match("^https?://([^/:]+):?(%d*)")
     if not host then
       ngx.status = 400
       ngx.header["Content-Type"] = "application/json"
       ngx.say('{"error":"Target-URL invalide"}')
       return ngx.exit(400)
     end

     -- Cibles LAN explicitement autorisées (widgets Dashy internes)
     local allowed_internal = { ["<IP_SERVEUR>"] = true, ["<IP_ADGUARD>"] = true, ["<IP_PICO>"] = true }
     local function is_private_ip(h)
       if h == "localhost" then return true end
       local a, b = h:match("^(%d+)%.(%d+)%.%d+%.%d+$")
       if not a then return false end -- pas une IPv4 littérale -> traité comme hôte public
       a, b = tonumber(a), tonumber(b)
       if a == 127 or a == 10 or a == 0 then return true end
       if a == 172 and b >= 16 and b <= 31 then return true end -- 172.16.0.0/12 (docker inclus)
       if a == 192 and b == 168 then return true end
       if a == 169 and b == 254 then return true end -- link-local / metadata cloud
       return false
     end

     -- Anti-SSRF : on bloque tout le LAN/loopback/metadata, sauf les cibles explicitement whitelistées
     if is_private_ip(host) and not allowed_internal[host] then
       ngx.status = 403
       ngx.header["Content-Type"] = "application/json"
       ngx.say('{"error":"Cible interne non autorisée"}')
       return ngx.exit(403)
     end

     ngx.var.host_override = (port ~= "" and port ~= nil) and (host .. ":" .. port) or host

     -- Parse le JSON envoyé dans customheaders
     local cjson = require "cjson.safe"
     local hdrs = ngx.req.get_headers()
     local ch = hdrs["customheaders"]
     if ch then
       local obj = cjson.decode(ch)
       if obj then
         if obj["Authorization"] then ngx.var.auth_header = obj["Authorization"] end
         if obj["Accept"] then ngx.var.accept_header = obj["Accept"] end
         if obj["OCS-APIREQUEST"] ~= nil then
           ngx.var.ocs_header = tostring(obj["OCS-APIREQUEST"])
         end
       end
     end
   }

   # Injecte les vrais headers vers la cible
   proxy_set_header Authorization $auth_header;
   proxy_set_header Accept $accept_header;
   proxy_set_header OCS-APIREQUEST $ocs_header;

   proxy_set_header Host $host_override;
   proxy_set_header X-Forwarded-Proto https;

   proxy_pass $target;
}

   # Application (SPA Vue en history mode)
   location / {
       include /config/nginx/authelia-location.conf;
       include /config/nginx/proxy.conf;
       include /config/nginx/resolver.conf;
       proxy_pass $upstream_proto://$upstream_app:$upstream_port;

       # --- Fallback SPA : si l’upstream renvoie 404 sur /login, /settings, etc.,
       #     on renvoie l'index de l'app pour laisser Vue router côté client.
       proxy_intercept_errors on;
       error_page 404 = @dashy_fallback;
    }

   # Cible du fallback : renvoyer l'index de l'app en conservant les en-têtes
   location @dashy_fallback {
       proxy_set_header X-Original-URI $request_uri;
       proxy_pass $upstream_proto://$upstream_app:$upstream_port/;
    }
}
  • Authelia protège tout le domaine, y compris les fichiers YAML, qui contiennent les identifiants des widgets.
  • /cors-proxy remplace le proxy CORS de Dashy pour les widgets en useProxy: true. Il ne relaie que les en-têtes Authorization, Accept et OCS-APIREQUEST, et refuse les IP privées absentes de allowed_internal : tout nouveau widget qui vise une IP du LAN doit y être ajouté.
  • /pico/ relaie l'interface web du Pico W.
  • @dashy_fallback renvoie l'application sur les routes internes de Vue (/login...), que le serveur de Dashy renvoie en 404.

Authelia

Règle dans la section access_control de la configuration d'Authelia :

access_control:
 default_policy: deny
 rules:
    - domain: "dash.<DOMAINE>"
     policy: one_factor

Aucune règle bypass pour ce domaine ne doit la précéder : Authelia applique la première règle qui correspond.

Fichiers de configuration

Dans <APPDATA>/Dashy/user-data :

Page DashyFichierSous-pageWidgets avec identifiants
Accueilconf.ymlAccueiladguard-stats, adguard-filter-status, adguard-top-domains (compte AdGuard Home) ; nextcloud-system (mot de passe d'application Nextcloud)
bookmarksbookmark.ymlBookmarks—
cpucpu.ymlCPU—
newsnews.ymlNewsflight-data (clé RapidAPI) ; news-headlines (clé Currents API)

item-icons/ contient les icônes locales et l'image de fond.

Multi-pages

La page d'accueil est conf.yml. Elle déclare les pages secondaires dans pages: : nom affiché dans la barre de navigation et chemin relatif à user-data.

pages:
  - name: bookmarks
   path: bookmark.yml
  - name: cpu
   path: cpu.yml
  - name: news
   path: news.yml

Chaque page secondaire ne contient que pageInfo, sections et la même liste pages:, pour garder la navigation. appConfig n'est lu que dans conf.yml et s'applique à toutes les pages.

Fichiers présents mais non référencés : cpu2.yml (ancienne copie de cpu.yml à l'indentation cassée), conf.yml.bak et bookmark.yml.bak.

Éléments communs

appConfig est dans conf.yml, dont le fichier complet est sur la sous-page Accueil.

  • Thème dracula, image de fond servie depuis item-icons/.
  • statusCheck: false au niveau global : la vérification d'état est activée item par item.
  • faviconApi: allesedv pour les items en icon: favicon. enableFontAwesome: true pour les icônes fas, fab et fad. Les icônes hl-... viennent du jeu homelab intégré.
  • widgetsAlwaysUseProxy: false : chaque widget choisit avec useProxy s'il passe par /cors-proxy. Le réglage se met sur le widget : au niveau de la section, il est ignoré.
  • Authentification : un compte admin défini par son nom et le hash SHA-256 de son mot de passe (echo -n '<MOT_DE_PASSE>' | sha256sum). enableGuestAccess: true : une fois passé Authelia, on voit les pages sans se connecter à Dashy. disableConfigurationForNonAdmin: true : seul l'admin modifie la configuration.

Vérifier la syntaxe après une modification à la main :

docker exec dashy yarn validate-config

Statistiques GitHub : github-readme-stats

Instance locale de github-readme-stats, qui génère les cartes SVG de statistiques GitHub affichées par un widget de Dashy : statistiques du profil, langages les plus utilisés, dépôts épinglés.

services:
 github-readme-stats:
   image: github-readme-stats:local
   container_name: github-readme-stats
   restart: unless-stopped
   ports:
      - "9000:9000"
   environment:
      - PAT_1=${PAT_1}
      - CACHE_SECONDS=14400
  • PAT_1 : jeton d'accès personnel GitHub, défini dans les variables de la stack Portainer. Jeton classique avec les permissions repo pour compter les dépôts privés.
  • CACHE_SECONDS : durée de mise en cache des cartes (4 heures), pour limiter les appels à l'API GitHub.
  • Accessible uniquement en local, sur le port 9000.

Principales adresses :

CarteAdresse
Statistiques du profil/api?username=<UTILISATEUR>
Langages les plus utilisés/api/top-langs?username=<UTILISATEUR>
Dépôt épinglé/api/pin?username=<UTILISATEUR>&repo=<DEPOT>

Dans Dashy, les cartes s'affichent avec un widget de type image qui pointe vers l'instance locale.

Image locale

Portainer ne sait pas construire une image dans une stack : l'image github-readme-stats:local est construite sur l'hôte, depuis le dépôt cloné. Elle n'est donc pas mise à jour par Watchtower. Pour la mettre à jour, depuis le dossier du dépôt :

git pull
docker build -t github-readme-stats:local .

Puis redéployer la stack dans Portainer.

Maintenance

Après une migration ou une réinstallation

ServiceÀ refaire
NextcloudCréer un nouveau mot de passe d'application et le reporter dans nextcloud-system : les mots de passe d'application ne suivent pas une migration.
AdGuard HomeReporter le compte de l'interface web dans les trois widgets adguard-*. Si l'IP change : hostname et allowed_internal de SWAG.
Pico WSi l'IP change : news.yml (item et script), location /pico/ et allowed_internal de SWAG.
SWAG, AutheliaRemettre 30-dashy.subdomain.conf complet et la règle one_factor, puis vérifier en navigation privée que /conf.yml redirige vers Authelia.
DashyRestaurer user-data.

Recharger la configuration

  • Dashy relit les fichiers YAML à chaque chargement : après une modification à la main, rafraîchir la page. Si l'ancienne version reste affichée, elle vient du stockage local du navigateur : réinitialiser les paramètres locaux depuis le menu de configuration.
  • SWAG : docker exec swag nginx -t puis docker exec swag nginx -s reload. Authelia : docker restart authelia.

Un widget reste vide

Dans les outils de développement du navigateur (F12, onglet Réseau), regarder la requête du widget :

SymptômeCause
Erreur CORS sur un appel directPasser le widget en useProxy: true
/cors-proxy : 302 ou 401Session Authelia expirée : recharger la page
/cors-proxy : 403 Cible interne non autoriséeIP absente de allowed_internal
/cors-proxy : 401 ou 403 de la cibleIdentifiants faux ou révoqués, compte sans droits, ou en-tête non relayé par le bloc Lua
/cors-proxy : 502 ou 504Service cible arrêté ou injoignable depuis SWAG

Journaux : docker logs dashy et <APPDATA>/swag/config/log/nginx/error.log.

Sauvegarde

<APPDATA>/Dashy/user-data est sauvegardé par le script de Sauvegarde, identifiants des widgets compris. Le fichier SWAG est aussi dans <APPDATA>.