Dépannage¶
Cette page recense les pannes réellement rencontrées pendant la mise en place, avec leur cause racine. Elles sont représentatives : la plupart se reproduiront sur une installation neuve ou une montée de version.
PostgreSQL refuse de démarrer¶
Error: in 18+, these Docker images are configured to store database data in a
format which is compatible with "pg_ctlcluster" ... there appears to be
PostgreSQL data in: /var/lib/postgresql/data (unused mount/volume)
Cause. Depuis PostgreSQL 18, l'image officielle attend le volume monté sur
/var/lib/postgresql et non plus /var/lib/postgresql/data. Les données
vivent dans un sous-répertoire par version majeure, ce qui permet
pg_upgrade --link sans franchir de point de montage.
Correctif. Monter le volume sur /var/lib/postgresql.
Toutes les tâches Python d'Airflow échouent sur ModuleNotFoundError: airflow¶
Cause. L'image ajoute /opt/dbt/bin en tête du PATH. L'interpréteur
Python du virtualenv dbt masque alors celui d'Airflow, qui ne contient pas le
paquet airflow.
Correctif. Ne jamais mettre le virtualenv dbt dans le PATH. Exposer
DBT_BIN=/opt/dbt/bin/dbt et l'appeler par son chemin absolu.
number of parameters must be between 0 and 65535¶
Cause. DataFrame.to_sql(method="multi") construit un INSERT géant dont
chaque valeur est un paramètre lié. Le protocole PostgreSQL en plafonne le
nombre à 65 535 : la limite est atteinte à chunksize × nb_colonnes. Le bug ne
se déclenche donc que sur certaines tables, selon leur largeur.
Correctif. Utiliser COPY, qui n'a pas cette limite et charge un ordre de
grandeur plus vite. Voir dags/lib/warehouse.py.
cannot drop table bronze.raw_customers because other objects depend on it¶
Cause. to_sql(if_exists="replace") exécute un DROP TABLE, or les vues
de la couche silver dépendent des tables bronze.
Correctif. Sur remplacement, vider la table (TRUNCATE) au lieu de la
supprimer. Le DROP ... CASCADE n'est utilisé qu'en dernier recours, quand la
structure a réellement changé — dbt reconstruit alors les vues.
column "eolien" is of type bigint but expression is of type text¶
Cause. L'API ODRÉ est faiblement typée : selon les jours, une même colonne
arrive en entier ou en chaîne ("0"). pandas déduit les types lot par lot,
si bien que la table du jour et la table cible divergent.
Correctif. Imposer le schéma à l'ingestion :
pd.to_numeric(..., errors="coerce") sur les colonnes de mesure,
astype("string") sur les colonnes de libellé.
function round(double precision, integer) does not exist¶
Cause. PostgreSQL ne fournit round(x, n) que pour numeric. Le modèle
référençait la colonne source en bronze, typée double precision par
pandas — et non l'alias casté de la même requête.
Correctif. Caster explicitement : round(expression::numeric, 4).
Portabilité
DuckDB, BigQuery et Snowflake acceptent la forme non castée. Un modèle qui fonctionne sur l'un peut échouer sur PostgreSQL.
L'API ODRÉ renvoie 400 IncompatibleTypesInComparisonFilter¶
Cause. Le champ date du jeu eco2mix-regional-tr est typé texte. Une
comparaison avec un littéral date (date = date'2026-08-18') est rejetée.
Correctif. Comparer des chaînes : where=date = "2026-08-18".
Le DAG d'ingestion échoue en déclenchement manuel¶
Cause. En Airflow 3, une exécution déclenchée manuellement n'a pas
forcément d'intervalle de données : data_interval_start vaut None.
Correctif. Prévoir un repli — paramètre explicite, sinon intervalle, sinon la veille. Le bouton « Trigger » de l'interface doit fonctionner sans configuration.
La transformation dbt ne se déclenche plus toute seule¶
Cause. schedule=[ASSET_A, ASSET_B] signifie ET : Airflow attend que
tous les assets aient été mis à jour. Le jeu synthétique n'étant rechargé que
manuellement, la transformation attendait indéfiniment.
Correctif. Utiliser un OU logique : schedule=ASSET_A | ASSET_B.
Panne silencieuse
Aucune erreur n'est levée : le DAG reste simplement à l'arrêt. C'est exactement ce que la planification par asset est censée éviter.
Traefik renvoie 404 sur des chemins pourtant configurés¶
Cause. Les règles sont portées par les étiquettes Docker du conteneur.
Modifier le docker-compose.yml ne suffit pas : tant que le conteneur n'est
pas recréé, il porte les anciennes étiquettes.
Correctif.
docker compose up -d --force-recreate <service>
docker inspect metrika-<service>-1 --format '{{json .Config.Labels}}' | python3 -m json.tool
Laisser aussi quelques secondes à Traefik pour recharger sa configuration.
L'URI d'un asset Airflow est rejetée¶
Cause. Les providers normalisent les URI d'asset selon leur schéma. Un
postgres:// doit respecter la forme hôte/base/schéma/table.
Correctif. Pour un identifiant purement logique, utiliser un nom simple :
Asset(name="warehouse.bronze.demo").
Metabase n'envoie pas de courriel malgré email-configured? = true¶
Cause. La route PUT /api/email n'accepte que les paramètres smtp-*.
L'adresse d'expéditeur reste vide, et les messages partent sans expéditeur
déclaré — donc massivement classés en indésirables.
Correctif. Poser email-from-address, email-from-name et
email-reply-to via PUT /api/setting/<clé>.
Un tableau de bord intégré en iframe reste blanc¶
Cause. Le gestionnaire d'événement load vidait le conteneur
(innerHTML = '') puis y réinsérait l'iframe. Vider le conteneur détache
l'iframe du document ; la réinsérer la fait recharger, ce qui redéclenche
load — et boucle indéfiniment sur un écran blanc.
Le même tableau de bord ouvert en plein écran fonctionnait, puisqu'il n'y avait alors aucune iframe.
Correctif. Insérer l'iframe une seule fois, et retirer uniquement le message d'attente au chargement.
Une barre SVG animée déborde en diagonale¶
Cause. En SVG, transform-origin se calcule par rapport au repère du SVG
entier, et non par rapport à l'élément — contrairement au HTML. Une animation
scaleX(0) → scaleX(1) avec transform-origin: left center part donc du coin
du graphique.
Correctif. Ajouter transform-box: fill-box sur l'élément animé.
Metabase refuse une carte GeoJSON personnalisée¶
Trois causes distinctes, dans l'ordre où on les rencontre :
| Message | Cause | Correctif |
|---|---|---|
| « doit commencer par http:// ou https:// … hôtes internes interdits » | URL pointant vers le réseau Docker (http://web/…) — protection contre les requêtes côté serveur |
Servir le fichier sur une URL publique |
| « content-type invalide » | nginx ne connaît pas l'extension .geojson et sert en application/octet-stream |
Déclarer types { application/geo+json geojson; } |
| Carte figée sur un chargement perpétuel | Le navigateur appelle /api/geojson/<clé> |
S'assurer que ce chemin est routé vers Metabase |
Le contenu d'une page n'apparaît pas en capture ou à l'impression¶
Cause. Les animations d'apparition au défilement mettent le contenu à
opacity: 0 en attendant l'IntersectionObserver. Une capture pleine page ne
déclenche aucun défilement : rien n'apparaît jamais.
Correctif. Ne masquer que si l'observateur existe réellement, révéler tout
à l'événement beforeprint, et prévoir un filet de sécurité qui affiche tout
au bout de quelques secondes.
Une série temporelle synthétique paraît multipliée par quarante¶
Cause. Deux artefacts de génération cumulés :
- les clients étaient créés uniformément sur la fenêtre observée — les premiers mois n'avaient donc presque aucun client ;
- la saisonnalité était appliquée après coup, en décalant une partie des commandes de fin d'année vers une date ultérieure tirée au hasard. Cela déplace systématiquement la masse vers la fin de la période.
Correctif. Faire préexister les deux tiers du parc à la fenêtre observée, et tirer le mois de commande par échantillonnage pondéré (coefficient saisonnier × croissance) plutôt que de déplacer une date déjà tirée.
Débordement horizontal sur mobile¶
Symptôme. document.documentElement.scrollWidth dépasse innerWidth.
Diagnostic. Parcourir les éléments et repérer ceux dont le bord droit dépasse la fenêtre :
[...document.querySelectorAll('body *')]
.filter(e => e.getBoundingClientRect().right > innerWidth + 1)
.map(e => `${e.tagName}.${e.className}`);
Dans le cas rencontré, une barre de navigation sans flex-wrap dont le dernier
lien dépassait de 41 px.