Mettre en place un environnement de développement et d'exécution pour Dbt-Core
Introduction
DBT Core (Data Build Tool) s’est imposé comme un outil de référence pour la modélisation et la transformation des données. Il permet d’appliquer des transformations directement dans le Data Warehouse en suivant une approche SQL-first et un paradigme ELT (Extract-Load-Transform). Il y a quelques mois, j’ai choisi d’intégrer cette technologie à ma stack afin de bénéficier d’un outil spécialisé dans la modélisation et la transformation, d’améliorer les performances, de rendre les modèles plus lisibles grâce au "lineage" et de générer facilement une documentation claire.

Dans cet article, nous allons voir comment :
- Mettre en place un environnement de développement local reproductible avec un DevContainer dans VS Code.
- Construire une image Docker personnalisée de DBT Core pour exécuter un projet dans un environnement standardisé.
- Puis utiliser cette image dans Apache Airflow à l’aide du DockerOperator afin d’automatiser l’exécution des modèles.

Pré-requis
- Docker ou Docker Desktop
- Plugin Dev Container

DevContainer
Plutot que de multiplier les environnements virtuels python sur ma machine dev, j'ai décidé de mettre en place des Devcontainers pour me faciliter la vie. Dans cet article, je présente ma solution pour dbt mais j’adopte la même approche pour mes projets purement python par exemple. Ma solution n'est peut être pas encore la plus mature ou la plus optimisée mais c'est une première étape.

devcontainer.json
La première étape consiste à créer un dossier .devcontainer dans votre projet, puis à y ajouter un fichier devcontainer.json, ainsi qu’un requirements.txt si nécessaire. Le fichier devcontainer.json définit l’environnement de travail à utiliser dans le Dev Container. Vous pouvez bien sûr le personnaliser : ajouter des extensions, configurer des variables d’environnement, et bien plus encore.
{
"name": "Dbt-Core SQL-Server",
"runArgs": [
"--dns=ip", # pas forcement utile
"--dns-search=tondomain" # pas forcement utile
],
"build": {
"dockerfile": "Dockerfile",
"context": "."
},
"workspaceFolder": "/app",
"postCreateCommand": "pip install -r /app/requirements.txt", # peut se mettre directement dans le Dockerfile
"mounts": [
"source=${localWorkspaceFolder},target=/app,type=bind"
],
"customizations": {
"vscode": {
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python"
},
"extensions": [
"ms-python.python",
"innoverio.vscode-dbt-power-user",
"dorzey.vscode-sqlfluff",
"oderwat.indent-rainbow"
]
}
},
"remoteUser": "root",
"remoteEnv": {
"SERVER": "hostname",
"UID": "user",
"PASS": "password",
"DATABASE": "DBNAME",
"SCHEMA": "dbo",
}
}
requirements.txt
dbt-core==1.10.5
dbt-sqlserver==1.9.0
sqlfluff
sqlfluff-templater-dbt
Le Dev Container démarre avec plusieurs extensions VS Code préinstallées :
- "dbt-power-user" pour simplifier le développement avec dbt,
- "sqlfluff" afin d’assurer un formatage SQL cohérent,
- "indent-rainbow" pour visualiser facilement les niveaux d’indentation.

sqlfluff
Dans le Fichier : .sqlfluff à la racine du projet :
[sqlfluff]
dialect = tsql
templater = dbt
max_line_length = 500
tab_space_size = 4
[sqlfluff:templater:dbt]
project_dir = ./
Vscode
Dans le fichier : .vscode/settings.json
{
"dbt.enableNewLineagePanel": true,
"sqlfluff.executablePath": "sqlfluff",
"sqlfluff.dialect": "tsql",
"sqlfluff.linter.run": "onSave",
"sqlfluff.experimental.format.executeInTerminal": true,
"sqlfluff.format.enabled": true,
"sqlfluff.ignoreLocalConfig": false,
"editor.formatOnSave": false,
"[sql]": {
"editor.defaultFormatter": "dorzey.vscode-sqlfluff"
}
}
Dockerfile
Seconde étape, créer un fichier Dockerfile. le devcontainer sera basé sur ce Dockerfile. Dans cet exemple, il est basé sur python 3.11 slim, je le mets en local fr + installation de quelques packages et surtout du driver odbc 18 pour me connecter à Sql-Server.
FROM python:3.11-slim
# Variables d'environnement
ENV LANG=fr_FR.UTF-8 \
LANGUAGE=fr_FR:fr \
LC_ALL=fr_FR.UTF-8 \
TZ=Europe/Paris \
DEBIAN_FRONTEND=noninteractive
# Installer tzdata, locales et unzip, configurer timezone et locale
RUN apt-get update && \
apt-get install -y locales tzdata unzip && \
ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata && \
sed -i '/fr_FR.UTF-8/s/^# //g' /etc/locale.gen && \
locale-gen fr_FR.UTF-8 && \
update-locale LANG=fr_FR.UTF-8 && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# --- Installation du pilote ODBC Microsoft SQL Server 18 ---
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
gnupg2 \
ca-certificates \
git \
gcc \
build-essential && \
curl -sSL https://packages.microsoft.com/keys/microsoft.asc | \
gpg --dearmor -o /usr/share/keyrings/microsoft-archive-keyring.gpg && \
echo "deb [signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] https://packages.microsoft.com/debian/12/prod bookworm main" \
> /etc/apt/sources.list.d/mssql-release.list && \
apt-get update && \
ACCEPT_EULA=Y apt-get install -y --no-install-recommends \
msodbcsql18 \
unixodbc-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
WORKDIR /app
Nous disposons désormais d’un environnement de développement reproductible sur d’autres machines et indépendant du système hôte. Avec quelques ajustements, cela fonctionne également avec les éditeurs JetBrains.
VS Code détecte automatiquement la présence du fichier devcontainer.json et propose de builder et redémarrer le DevContainer.
Docker personnalisé
Nous avons notre environnement de développement, il nous faut maintenant un environnement d'exécution. Nous allons voir comment créer une image docker pour exécuter les commandes dbt.
La base est la même que le Dockerfile utilisé pour le DevContainer sauf qu'on y integre directement les packages nécessaires pour Dbt et Sql-Server.
FROM python:3.11-slim
# Variables d'environnement
ENV LANG=fr_FR.UTF-8 \
LANGUAGE=fr_FR:fr \
LC_ALL=fr_FR.UTF-8 \
TZ=Europe/Paris \
DEBIAN_FRONTEND=noninteractive
# Installer tzdata, locales et unzip, configurer timezone et locale
RUN apt-get update && \
apt-get install -y locales tzdata unzip && \
ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata && \
sed -i '/fr_FR.UTF-8/s/^# //g' /etc/locale.gen && \
locale-gen fr_FR.UTF-8 && \
update-locale LANG=fr_FR.UTF-8 && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# --- Installation du pilote ODBC Microsoft SQL Server 18 ---
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
gnupg2 \
ca-certificates \
git \
gcc \
build-essential && \
curl -sSL https://packages.microsoft.com/keys/microsoft.asc | \
gpg --dearmor -o /usr/share/keyrings/microsoft-archive-keyring.gpg && \
echo "deb [signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] https://packages.microsoft.com/debian/12/prod bookworm main" \
> /etc/apt/sources.list.d/mssql-release.list && \
apt-get update && \
ACCEPT_EULA=Y apt-get install -y --no-install-recommends \
msodbcsql18 \
unixodbc-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# --- Configuration de l'environnement Python et installation des paquets ---
RUN pip install --no-cache-dir \
dbt-core==1.10.5 \
dbt-sqlserver==1.9.0
WORKDIR /app
Build
Si vous voulez que cette image soit disponible sur différent serveurs ou environnements, il serait préférable de la rendre disponible sur un dépôt docker en public ou en privé selon ce que vous mettez dans cette image.
Dans un terminal, placez-vous dans le répertoire ou se trouve le Dockerfile et lancer le build.
docker build -t dbt:dbtsqlserver_v1 .
Tag
Pour rendre l’image accessible et réutilisable depuis n’importe quel environnement, il est possible de la publier sur un dépôt Docker. À défaut, il reste possible de la construire localement sur chaque machine qui devra l’exécuter à l’aide du Dockerfile et de la commande build.
Avant de pousser une image vers un dépôt, il est indispensable de taguer correctement l’image, sans quoi l’opération de push échouera.
docker login
docker tag dbt:dbtsqlserver_v1 account/repo:dbtsqlserver_v1
docker push account/repo:dbtsqlserver_v1
Airflow
J'ai volontairement sauté la partie ci/cd puisqu'elle est vraiment spécifique à chacun. Tout est prêt, notre projet dbt fonctionne bien localement, notre environnement d'exécution est prêt. Il faut maintenant créer notre dag sur Apache Airflow pour orchestrer nos modèles.
Gestion des secrets
Comme tout projet dbt, une connexion au Data Warehouse nécessite des identifiants et credentials. Ces variables peuvent être stockées dans un fichier .env et injectées dynamiquement dans le conteneur à chaque exécution, garantissant ainsi une gestion simple et sécurisée des secrets.
create_env_file.py
import yaml
import sys
from pathlib import Path
def create_env_file(yaml_file="secrets.yml", env_file=".env"):
"""Lit un fichier YAML et crée un fichier .env"""
# Vérifie si le fichier YAML existe
if not Path(yaml_file).exists():
print(f"❌ Fichier {yaml_file} introuvable")
return False
try:
# Charge le fichier YAML
with open(yaml_file, 'r', encoding='utf-8') as f:
variables = yaml.safe_load(f)
# Crée le fichier .env
with open(env_file, 'w', encoding='utf-8') as f:
print(f"Création du fichier {env_file}...")
for key, value in variables.items():
f.write(f'export {key}="{value}"\n')
print(f"-> {key} exportée")
print(f"✅ Variables exportées dans {env_file}")
return True
except Exception as e:
print("❌ Une erreur est survenue lors de la création du fichier .env")
print(f"❌ Erreur : {e}")
return False
if __name__ == "__main__":
res = create_env_file("secrets.yml")
if res == False:
sys.exit(1)
Ce script Python génère un fichier .env, que l’on peut ensuite charger via la commande source .env. Les variables à y inclure sont définies dans un fichier secrets.yml, qui reste volontairement exclu du dépôt Git du projet dbt afin de protéger les informations sensibles. L'idéal serait d'utiliser un gestionnaire de secrets.
Nous pouvons à présent passer à l’orchestration du projet dbt avec Apache Airflow. Chaque tâche du DAG correspondra directement à une commande dbt, telle que dbt run, dbt test. Pour plus d'efficience, on pourra lancer uniquement la commande dbt build. La commande build s'occupe d'exécuter pour chaque modèle le run et ses tests avant de passer au modèle suivant. Cela permet d'éviter de lancer l’ensemble des modèles si certains tests échouent et ainsi éviter de "contaminer" tout le reste du warehouse avec des mauvaises data.
/bin/bash -c "python create_env_file.py && source .env && dbt run --target prod
/bin/bash -c "python create_env_file.py && source .env && dbt test --target prod
DAG
from datetime import datetime, timedelta
import pendulum
from airflow import DAG
from airflow.operators.docker_operator import DockerOperator
from docker.types import Mount
#from airflow.operators.python_operator import PythonOperator
#from airflow.operators.dummy_operator import DummyOperator
#from airflow.operators.empty import EmptyOperator
#from airflow.operators.bash import BashOperator
#from airflow.providers.ssh.operators.ssh import SSHOperator
##########################################################################
# MODIFS #
# Createur : ndl
# Historique:
# 29/07/2025 - NDU - test
##########################################################################
# DEFINITION GLOBALE DU DAG #
DAG_ID = 'dbt_test_docker'
DAG_NAME = 'dbt_test_docker'
DAG_TAG = ['dbt','docker','modelisation']
DAG_TIMEOUT = 10
DAG_RETRY_DELAY = 1
DAG_RETRIES = 0
DOCKER_TIMEOUT = 4
DOCKER_CPU = 1
DOCKER_RAM = '512m'
default_args = {
'owner' : 'ndl',
'description' : DAG_NAME,
'depend_on_past' : False,
'start_date' : datetime(2025, 7, 18),#pendulum.today(config.TIMEZONE).add(days=-1), # annee / mois / jour (sans le 0)
'email_on_failure' : False,
'email_on_retry' : False,
'retries' : DAG_RETRIES,
'retry_delay' : timedelta(minutes=DAG_RETRY_DELAY),
}
##########################################################################
# DECLARATION DU DAG #
with DAG(DAG_ID,
dag_display_name = DAG_NAME,
default_args = default_args,
catchup = False,
tags= DAG_TAG,
schedule_interval = None, # https://crontab.cronhub.io/
is_paused_upon_creation = True,
max_active_runs=1,
dagrun_timeout=timedelta(minutes=DAG_TIMEOUT),
on_failure_callback=failure_callbacks,
on_success_callback=None,
) as dag:
##########################################################################
# DECLARATION DES TASKS #
# Parametres projet DBT
DbtConfigDocker = {
'image': 'account/repo:dbtsqlserver_v1',
'cpus': DOCKER_CPU,
'mem_limit': DOCKER_RAM,
'command_run': '/bin/bash -c "python create_env_file.py && source .env && dbt run --target prod"',
'command_test': '/bin/bash -c "python create_env_file.py && source .env && dbt test --target prod"',
'priority_weight':config.PriorityWeight('normal'),
'execution_timeout':timedelta(minutes=DOCKER_TIMEOUT),
'source_project':f'{env.DBT_DOCKER_BASE_MOUNT}/Lavages',
'api_url':'http://host.docker.internal:8000',
'docker_url' : env.DOCKER_URL, # "tcp://host.docker.internal:2375"
}
dbt_run = DockerOperator(
task_id='dbt_run',
image = DbtConfigDocker['image'],
command = DbtConfigDocker['command_run'],
api_version='auto',
mount_tmp_dir=False,
auto_remove=True,
cpus= DbtConfigDocker['cpus'],
mem_limit= DbtConfigDocker['mem_limit'],
priority_weight= DbtConfigDocker['priority_weight'],
execution_timeout= DbtConfigDocker['execution_timeout'],
#docker_url="unix://var/run/docker.sock",
docker_url=DbtConfigDocker['docker_url'],
network_mode="bridge",
mounts=[Mount(source=DbtConfigDocker['source_project'], target='/app', type='bind')],
force_pull=False,
)
dbt_test = DockerOperator(
task_id='dbt_test',
image = DbtConfigDocker['image'],
command = DbtConfigDocker['command_test'],
api_version='auto',
mount_tmp_dir=False,
auto_remove=True,
cpus= DbtConfigDocker['cpus'],
mem_limit= DbtConfigDocker['mem_limit'],
priority_weight= DbtConfigDocker['priority_weight'],
execution_timeout= DbtConfigDocker['execution_timeout'],
#docker_url="unix://var/run/docker.sock",
docker_url=DbtConfigDocker['docker_url'],
network_mode="bridge",
mounts=[Mount(source=DbtConfigDocker['source_project'], target='/app', type='bind')],
force_pull=False
)
##########################################################################
# ORCHESTRATION DES TASKS #
dbt_run >> dbt_test
Résumé
En combinant DevContainer, Docker et Airflow, on obtient un environnement dbt reproductible, portable et facile à automatiser. Le DevContainer standardise le développement, l’image Docker fournit un environnement d’exécution cohérent, et Airflow permet d’orchestrer les modèles dbt.
Cette base fournit un bon point de départ solide pour travailler de manière organisée et fiable.