<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Roméo DOSsOu</title>
    <description>The latest articles on DEV Community by Roméo DOSsOu (@romeo_dossou_b9a9ace7ba7c).</description>
    <link>https://dev.to/romeo_dossou_b9a9ace7ba7c</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3997351%2Ff9d06c16-4302-4ef2-808e-1fe5243f5c15.png</url>
      <title>DEV Community: Roméo DOSsOu</title>
      <link>https://dev.to/romeo_dossou_b9a9ace7ba7c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/romeo_dossou_b9a9ace7ba7c"/>
    <language>en</language>
    <item>
      <title>Comment dockeriser une API FastAPI de A à Z — Dockerfile expliqué ligne par ligne</title>
      <dc:creator>Roméo DOSsOu</dc:creator>
      <pubDate>Wed, 08 Jul 2026 16:31:07 +0000</pubDate>
      <link>https://dev.to/romeo_dossou_b9a9ace7ba7c/comment-dockeriser-une-api-fastapi-de-a-a-z-dockerfile-explique-ligne-par-ligne-2f22</link>
      <guid>https://dev.to/romeo_dossou_b9a9ace7ba7c/comment-dockeriser-une-api-fastapi-de-a-a-z-dockerfile-explique-ligne-par-ligne-2f22</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Tu sais faire tourner une API FastAPI en local. Tu lances &lt;code&gt;uvicorn main:app --reload&lt;/code&gt;, tu ouvres &lt;code&gt;localhost:8000/docs&lt;/code&gt;, et tout fonctionne.&lt;/p&gt;

&lt;p&gt;Maintenant, comment tu fais tourner cette même API sur n'importe quelle machine, sans installer Python, sans configurer un environnement virtuel, sans que "ça marche chez moi mais pas chez toi" ?&lt;/p&gt;

&lt;p&gt;La réponse, c'est Docker.&lt;/p&gt;

&lt;p&gt;Dans cet article, on prend une API CRUD simple — gestion de produits — et on la dockerise de A à Z. Pas de raccourci. Pas d'image mystérieuse qu'on colle sans comprendre. Chaque ligne du Dockerfile sera expliquée, avec la raison exacte pour laquelle elle existe.&lt;/p&gt;

&lt;p&gt;À la fin, tu seras capable de dockeriser n'importe quelle API Python simple par toi-même.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ce qu'on va construire
&lt;/h2&gt;

&lt;p&gt;Une API de gestion de produits avec 4 endpoints :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /produits&lt;/code&gt; — créer un produit&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /produits&lt;/code&gt; — lister tous les produits&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /produits/{id}&lt;/code&gt; — récupérer un produit par son id&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DELETE /produits/{id}&lt;/code&gt; — supprimer un produit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stack : &lt;strong&gt;FastAPI + Uvicorn + SQLAlchemy + SQLite + Docker&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Repo GitHub : &lt;strong&gt;[lien]&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Étape 1 — L'API FastAPI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Structure du projet
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fastapi-produits/
├── Dockerfile
├── .dockerignore
├── requirements.txt
├── main.py
├── database.py
├── models.py
└── schemas.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;database.py&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_engine&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.ext.declarative&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;declarative_base&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.orm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sessionmaker&lt;/span&gt;

&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqlite:///./produits.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connect_args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check_same_thread&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;SessionLocal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sessionmaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;autocommit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autoflush&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;declarative_base&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SessionLocal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le &lt;code&gt;yield&lt;/code&gt; dans &lt;code&gt;get_db()&lt;/code&gt; est important : FastAPI ouvre la session avant la requête, la passe à la route, et la ferme proprement après — même en cas d'erreur.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;models.py&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Float&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Base&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Produit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;__tablename__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;produits&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="nb"&gt;id&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;nom&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;prix&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;schemas.py&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProduitCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;nom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;prix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProduitRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProduitCreate&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;

    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;from_attributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deux schémas distincts : ce que le client envoie (&lt;code&gt;ProduitCreate&lt;/code&gt;, sans id) et ce que l'API renvoie (&lt;code&gt;ProduitRead&lt;/code&gt;, avec id). Le client n'envoie jamais l'id — c'est la base qui le génère.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;main.py&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.orm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schemas&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_db&lt;/span&gt;

&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API Produits&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/produits&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;schemas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProduitRead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;creer_produit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;produit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;schemas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProduitCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;nouveau&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Produit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;produit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nouveau&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nouveau&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;nouveau&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/produits&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;schemas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProduitRead&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lister_produits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Produit&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/produits/{produit_id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;schemas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProduitRead&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lire_produit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;produit_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;produit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Produit&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Produit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;produit_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;produit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Produit non trouvé&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;produit&lt;/span&gt;

&lt;span class="nd"&gt;@app.delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/produits/{produit_id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;supprimer_produit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;produit_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;produit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Produit&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Produit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;produit_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;produit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Produit non trouvé&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;produit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;requirements.txt&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;fastapi&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=0.111.0&lt;/span&gt;
&lt;span class="py"&gt;uvicorn&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=0.29.0&lt;/span&gt;
&lt;span class="py"&gt;sqlalchemy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=2.0.30&lt;/span&gt;
&lt;span class="py"&gt;pydantic&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=2.7.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pourquoi épingler les versions avec &lt;code&gt;==&lt;/code&gt; ?&lt;/strong&gt; Pour garantir que le build est reproductible. Sans version fixe, pip installe la dernière version disponible — et une mise à jour inattendue peut casser l'app silencieusement.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Teste en local avant de continuer :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
uvicorn main:app &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;http://localhost:8000/docs&lt;/code&gt; doit afficher Swagger UI. Si c'est bon, on passe à Docker.&lt;/p&gt;




&lt;h2&gt;
  
  
  Étape 2 — Le piège du cache Docker
&lt;/h2&gt;

&lt;p&gt;Avant d'écrire le Dockerfile, il faut comprendre un concept qui change tout : &lt;strong&gt;le cache des couches&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Docker construit une image couche par couche. Chaque instruction (&lt;code&gt;FROM&lt;/code&gt;, &lt;code&gt;COPY&lt;/code&gt;, &lt;code&gt;RUN&lt;/code&gt;...) crée une couche. Si une couche n'a pas changé depuis le dernier build, Docker la réutilise depuis le cache — sans la reconstruire.&lt;/p&gt;

&lt;p&gt;La règle critique : &lt;strong&gt;dès qu'une couche change, toutes les couches suivantes sont reconstruites&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ L'erreur classique du débutant
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .                              # copie tout le code&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt   &lt;span class="c"&gt;# installation après le code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Résultat : tu modifies une ligne dans &lt;code&gt;main.py&lt;/code&gt; → Docker voit que &lt;code&gt;COPY . .&lt;/code&gt; a changé → il réinstalle toutes les dépendances. À chaque modification.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ L'ordre correct
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .               # copie UNIQUEMENT requirements.txt&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt   &lt;span class="c"&gt;# cette couche est mise en cache&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .                              # le code vient APRÈS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Résultat : tu modifies &lt;code&gt;main.py&lt;/code&gt; → &lt;code&gt;requirements.txt&lt;/code&gt; n'a pas changé → Docker réutilise la couche &lt;code&gt;pip install&lt;/code&gt; → rebuild en 2 secondes.&lt;/p&gt;

&lt;p&gt;Sur une connexion lente, ce détail peut faire passer de 3 minutes à 3 secondes par rebuild.&lt;/p&gt;




&lt;h2&gt;
  
  
  Étape 3 — Le Dockerfile, ligne par ligne
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. IMAGE DE BASE&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11-slim&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point de départ : une image Linux avec Python 3.11 déjà installé. La version &lt;code&gt;slim&lt;/code&gt; fait ~150MB au lieu de ~1GB pour la version complète. Elle contient tout ce qu'il faut pour faire tourner Python, sans les outils de développement inutiles.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# 2. RÉPERTOIRE DE TRAVAIL&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Crée le dossier &lt;code&gt;/app&lt;/code&gt; dans le conteneur et le définit comme répertoire courant. Sans ça, les fichiers atterrissent à la racine &lt;code&gt;/&lt;/code&gt; — mélangés avec les fichiers système Linux.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# 3. DÉPENDANCES (stratégie de cache)&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copie uniquement &lt;code&gt;requirements.txt&lt;/code&gt; puis installe les dépendances. &lt;code&gt;--no-cache-dir&lt;/code&gt; supprime le cache interne de pip — il ne servira jamais dans un conteneur et occupe inutilement de l'espace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# 4. CODE SOURCE&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copie le reste du projet dans &lt;code&gt;/app&lt;/code&gt;. Vient après pip install — c'est la stratégie de cache vue à l'étape 2.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# 5. PORT (documentation uniquement)&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;code&gt;EXPOSE&lt;/code&gt; ne fait &lt;strong&gt;rien&lt;/strong&gt; au niveau réseau. Il documente simplement le port que l'app utilisera. L'ouverture réelle se fait avec &lt;code&gt;-p&lt;/code&gt; dans &lt;code&gt;docker run&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# 6. COMMANDE DE DÉMARRAGE&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deux points importants ici :&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pourquoi Uvicorn ?&lt;/strong&gt; FastAPI est un framework, pas un serveur. Il définit les routes et la logique, mais il ne sait pas écouter sur un port réseau. Uvicorn est le serveur ASGI qui fait ce travail. Sans Uvicorn, FastAPI ne reçoit aucune requête.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pourquoi &lt;code&gt;--host 0.0.0.0&lt;/code&gt; ?&lt;/strong&gt; &lt;code&gt;127.0.0.1&lt;/code&gt; = "écoute uniquement les connexions internes à cette machine". Dans un conteneur, "cette machine" c'est le conteneur lui-même. Le navigateur de l'hôte ne peut pas y accéder. &lt;code&gt;0.0.0.0&lt;/code&gt; = "écoute tout le trafic réseau" — y compris ce qui vient de l'extérieur du conteneur.&lt;/p&gt;




&lt;h2&gt;
  
  
  Étape 4 — Builder et observer le cache
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; fastapi-produits &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sortie du terminal (premier build) :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[1/5] FROM python:3.11-slim          12.4s  ← téléchargement
[2/5] WORKDIR /app                    0.0s
[3/5] COPY requirements.txt .         0.0s
[4/5] RUN pip install ...            28.3s  ← installation
[5/5] COPY . .                        0.1s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Maintenant, modifie une ligne dans &lt;code&gt;main.py&lt;/code&gt; et rebuilde :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[1/5] FROM python:3.11-slim           0.0s  ← CACHED
[2/5] WORKDIR /app                    0.0s  ← CACHED
[3/5] COPY requirements.txt .         0.0s  ← CACHED
[4/5] RUN pip install ...             0.0s  ← CACHED ✅ (pas de réinstallation)
[5/5] COPY . .                        0.1s  ← seulement ça est reconstruit
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La stratégie de cache fonctionne. 2 secondes au lieu de 30.&lt;/p&gt;




&lt;h2&gt;
  
  
  Étape 5 — Lancer le conteneur
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 8000:8000 &lt;span class="nt"&gt;--name&lt;/span&gt; api-produits fastapi-produits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;-d&lt;/code&gt;&lt;/strong&gt; : mode détaché — libère le terminal&lt;br&gt;&lt;br&gt;
&lt;strong&gt;&lt;code&gt;-p 8000:8000&lt;/code&gt;&lt;/strong&gt; : relie le port 8000 de ta machine au port 8000 du conteneur&lt;br&gt;&lt;br&gt;
&lt;strong&gt;&lt;code&gt;--name&lt;/code&gt;&lt;/strong&gt; : nom lisible pour référencer le conteneur&lt;/p&gt;

&lt;p&gt;Pour voir que tout tourne :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps
docker logs api-produits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logs attendus :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;INFO:     Uvicorn running on http://0.0.0.0:8000
INFO:     Application startup complete.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Teste l'erreur classique&lt;/strong&gt; : lance le conteneur sans &lt;code&gt;-p&lt;/code&gt;, puis essaie d'accéder à &lt;code&gt;localhost:8000&lt;/code&gt;. Connexion refusée. L'app tourne parfaitement à l'intérieur, mais sans le mapping de port, le trafic externe n'entre pas.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Étape 6 — Tester via Swagger UI
&lt;/h2&gt;

&lt;p&gt;Ouvre &lt;code&gt;http://localhost:8000/docs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;L'interface Swagger générée automatiquement par FastAPI est disponible — identique à ce qu'on avait en local. La dockerisation n'a rien cassé.&lt;/p&gt;

&lt;p&gt;Teste le cycle CRUD complet :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;POST /produits&lt;/code&gt; — crée un produit avec &lt;code&gt;nom&lt;/code&gt;, &lt;code&gt;prix&lt;/code&gt;, &lt;code&gt;stock&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /produits&lt;/code&gt; — vérifie qu'il apparaît dans la liste&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /produits/1&lt;/code&gt; — récupère-le par son id&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DELETE /produits/1&lt;/code&gt; — supprime-le&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /produits/1&lt;/code&gt; → &lt;code&gt;404&lt;/code&gt; confirmé&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Maintenant, arrête le conteneur, supprime-le, et relances-en un nouveau :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker stop api-produits &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker &lt;span class="nb"&gt;rm &lt;/span&gt;api-produits
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 8000:8000 &lt;span class="nt"&gt;--name&lt;/span&gt; api-produits-2 fastapi-produits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GET /produits&lt;/code&gt; → liste vide. Les données ont disparu.&lt;/p&gt;

&lt;p&gt;C'est la limite principale de ce projet : la base SQLite est dans le conteneur. Quand le conteneur est supprimé, les données le sont aussi. La solution — les &lt;strong&gt;volumes Docker&lt;/strong&gt; — sera l'objet d'un article dédié.&lt;/p&gt;




&lt;h2&gt;
  
  
  Résumé — Le Dockerfile complet commenté
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Image de base Python minimale&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11-slim&lt;/span&gt;

&lt;span class="c"&gt;# Répertoire de travail dans le conteneur&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Dépendances en premier — stratégie de cache&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt;

&lt;span class="c"&gt;# Code source après les dépendances&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# Documentation du port (n'ouvre rien)&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8000&lt;/span&gt;

&lt;span class="c"&gt;# Uvicorn sur 0.0.0.0 — obligatoire pour l'accessibilité hors conteneur&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Limites documentées
&lt;/h2&gt;

&lt;p&gt;Ce Dockerfile est volontairement simple. Ce qu'il ne fait pas :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Limite&lt;/th&gt;
&lt;th&gt;Solution à venir&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Données non persistantes&lt;/td&gt;
&lt;td&gt;Volumes Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image non optimisée&lt;/td&gt;
&lt;td&gt;Multi-stage build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Processus en root&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;USER&lt;/code&gt; non-root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pas de health check&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;HEALTHCHECK&lt;/code&gt; Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;PostgreSQL + Docker Compose&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ces points seront couverts dans les prochains articles de la série.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Tu viens de dockeriser une API FastAPI de A à Z. Pas de magie. Chaque ligne a une raison d'être.&lt;/p&gt;

&lt;p&gt;Ce que tu comprends maintenant que tu ne comprenais pas avant :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;La différence entre image et conteneur&lt;/li&gt;
&lt;li&gt;Pourquoi FastAPI a besoin d'Uvicorn&lt;/li&gt;
&lt;li&gt;Comment le cache Docker fonctionne et comment l'exploiter&lt;/li&gt;
&lt;li&gt;Pourquoi &lt;code&gt;--host 0.0.0.0&lt;/code&gt; est obligatoire&lt;/li&gt;
&lt;li&gt;La différence entre &lt;code&gt;EXPOSE&lt;/code&gt; et &lt;code&gt;-p&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Le repo complet est disponible sur GitHub : &lt;strong&gt;&lt;a href="https://github.com/Bordley/fastapi-produits.git" rel="noopener noreferrer"&gt;https://github.com/Bordley/fastapi-produits.git&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Cet article fait partie d'une série sur Docker et DevOps orientée développeurs francophones.&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Suivant : Dockerfile Senior — multi-stage, non-root user, production-ready.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags Dev.to :&lt;/strong&gt; &lt;code&gt;docker&lt;/code&gt; &lt;code&gt;fastapi&lt;/code&gt; &lt;code&gt;python&lt;/code&gt; &lt;code&gt;devops&lt;/code&gt; &lt;code&gt;beginners&lt;/code&gt; &lt;code&gt;tutorial&lt;/code&gt; &lt;code&gt;api&lt;/code&gt; &lt;code&gt;backend&lt;/code&gt; &lt;code&gt;webdev&lt;/code&gt; &lt;code&gt;programming&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags Hashnode :&lt;/strong&gt; &lt;code&gt;docker&lt;/code&gt; &lt;code&gt;fastapi&lt;/code&gt; &lt;code&gt;python&lt;/code&gt; &lt;code&gt;devops&lt;/code&gt; &lt;code&gt;beginners&lt;/code&gt; &lt;code&gt;tutorial&lt;/code&gt; &lt;code&gt;rest-api&lt;/code&gt; &lt;code&gt;backend&lt;/code&gt; &lt;code&gt;web-development&lt;/code&gt; &lt;code&gt;containerization&lt;/code&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>fastapi</category>
      <category>python</category>
    </item>
    <item>
      <title>Dockeriser une API Flask de zéro sur Ubuntu : le guide complet pour ingénieurs africains</title>
      <dc:creator>Roméo DOSsOu</dc:creator>
      <pubDate>Thu, 02 Jul 2026 17:14:00 +0000</pubDate>
      <link>https://dev.to/romeo_dossou_b9a9ace7ba7c/dockeriser-une-api-flask-de-zero-sur-ubuntu-le-guide-complet-pour-ingenieurs-africains-kgi</link>
      <guid>https://dev.to/romeo_dossou_b9a9ace7ba7c/dockeriser-une-api-flask-de-zero-sur-ubuntu-le-guide-complet-pour-ingenieurs-africains-kgi</guid>
      <description>&lt;h1&gt;
  
  
  Dockeriser une API Flask de zéro sur Ubuntu : le guide complet
&lt;/h1&gt;

&lt;p&gt;Il y a un problème que tout développeur africain a vécu au moins une fois.&lt;/p&gt;

&lt;p&gt;Tu construis une app. Elle tourne parfaitement sur ta machine. Tu l'envoies en production — ou à ton collègue, ou au client. Elle plante.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Mais ça marchait chez moi…"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Docker est la solution définitive à ce problème. Dans ce guide, je te montre comment dockeriser une API Flask réelle, de bout en bout, sur Ubuntu — avec les bonnes pratiques de production dès le départ.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ce qu'on va construire
&lt;/h2&gt;

&lt;p&gt;À la fin de ce guide, tu auras :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un projet Flask cloné depuis GitHub et analysé&lt;/li&gt;
&lt;li&gt;Un Dockerfile optimisé avec gestion intelligente du cache&lt;/li&gt;
&lt;li&gt;Une app servie par Gunicorn (le vrai serveur de prod, pas &lt;code&gt;flask run&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Une configuration sécurisée par variables d'environnement&lt;/li&gt;
&lt;li&gt;Une image versionnée et publiée sur Docker Hub&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prérequis
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ubuntu 22.04 ou 24.04&lt;/li&gt;
&lt;li&gt;Docker installé (&lt;a href="https://docs.docker.com/engine/install/ubuntu/" rel="noopener noreferrer"&gt;guide officiel&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Git installé&lt;/li&gt;
&lt;li&gt;Connaissances de base en Python et ligne de commande&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Étape 1 — Installer Docker sur Ubuntu
&lt;/h2&gt;

&lt;p&gt;Sur Ubuntu, Docker s'installe nativement — pas besoin de virtualisation comme sur Windows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Supprime les anciennes versions si présentes&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;pkg &lt;span class="k"&gt;in &lt;/span&gt;docker.io docker-doc docker-compose podman-docker containerd runc&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get remove &lt;span class="nv"&gt;$pkg&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# Installe Docker via le script officiel&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.docker.com &lt;span class="nt"&gt;-o&lt;/span&gt; get-docker.sh
&lt;span class="nb"&gt;sudo &lt;/span&gt;sh get-docker.sh

&lt;span class="c"&gt;# Autorise ton utilisateur à utiliser Docker sans sudo&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="nv"&gt;$USER&lt;/span&gt;
newgrp docker

&lt;span class="c"&gt;# Vérifie l'installation&lt;/span&gt;
docker run hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si tu vois &lt;code&gt;Hello from Docker!&lt;/code&gt; — tu es prêt.&lt;/p&gt;




&lt;h2&gt;
  
  
  Étape 2 — Cloner et analyser le projet Flask
&lt;/h2&gt;

&lt;p&gt;On part d'un projet existant, comme tu le ferais dans une vraie mission.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/gothinkster/flask-realworld-example-app.git
&lt;span class="nb"&gt;cd &lt;/span&gt;flask-realworld-example-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Avant de toucher quoi que ce soit, lis ces trois fichiers :&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;requirements.txt   &lt;span class="c"&gt;# Les dépendances Python&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;autoapp.py         &lt;span class="c"&gt;# Le point d'entrée de l'app&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;README.md          &lt;span class="c"&gt;# La documentation existante&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Ce réflexe — lire avant d'agir — est la marque d'un développeur qui pense avant de coder. C'est ce que cherche un tech lead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;La structure du projet :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flask-realworld-example-app/
├── autoapp.py          ← Point d'entrée Flask
├── requirements.txt    ← Dépendances Python
└── conduit/            ← Code de l'API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Étape 3 — Écrire le Dockerfile
&lt;/h2&gt;

&lt;p&gt;C'est l'étape centrale. Chaque instruction a une raison précise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Image de base : Python 3.10 allégée (150 Mo au lieu de 900 Mo)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.10-slim&lt;/span&gt;

&lt;span class="c"&gt;# Dossier de travail dans le container&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# ÉTAPE CLÉE : copier requirements.txt EN PREMIER&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;

&lt;span class="c"&gt;# Installer les dépendances sans cache pip&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# Installer Gunicorn — le serveur de production&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;gunicorn

&lt;span class="c"&gt;# Copier le code APRÈS les dépendances&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# Documenter le port utilisé&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 5000&lt;/span&gt;

&lt;span class="c"&gt;# Démarrer avec Gunicorn&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["gunicorn", "--bind", "0.0.0.0:5000", "autoapp:app"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pourquoi cet ordre précis ?
&lt;/h3&gt;

&lt;p&gt;Docker construit l'image couche par couche. Chaque instruction crée une couche mise en &lt;strong&gt;cache&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Si tu copies tout le code d'abord (&lt;code&gt;COPY . .&lt;/code&gt;), puis tu fais &lt;code&gt;pip install&lt;/code&gt; — le moindre changement dans ton code invalide le cache et force une réinstallation complète de toutes les dépendances.&lt;/p&gt;

&lt;p&gt;En copiant &lt;code&gt;requirements.txt&lt;/code&gt; seul en premier, le &lt;code&gt;pip install&lt;/code&gt; n'est relancé que si les dépendances changent. Si tu modifies juste ton code — le rebuild passe directement au &lt;code&gt;COPY . .&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;30 secondes économisées à chaque rebuild. Sur 50 rebuilds par semaine, ça compte.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pourquoi Gunicorn ?
&lt;/h3&gt;

&lt;p&gt;Le serveur intégré de Flask (&lt;code&gt;flask run&lt;/code&gt;) est :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mono-thread (une requête à la fois)&lt;/li&gt;
&lt;li&gt;Non sécurisé pour la production&lt;/li&gt;
&lt;li&gt;Prévu uniquement pour le développement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Gunicorn gère plusieurs workers en parallèle et est conçu pour la production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pourquoi &lt;code&gt;0.0.0.0&lt;/code&gt; ?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;127.0.0.1&lt;/code&gt; = accessible uniquement de l'intérieur du container → invisible depuis ton navigateur.&lt;br&gt;
&lt;code&gt;0.0.0.0&lt;/code&gt; = accessible depuis l'extérieur → le port mapping fonctionne.&lt;/p&gt;


&lt;h2&gt;
  
  
  Étape 4 — Créer le .dockerignore
&lt;/h2&gt;

&lt;p&gt;Sans ce fichier, Docker envoie &lt;strong&gt;tout&lt;/strong&gt; ton dossier au moteur de build — y compris les fichiers inutiles ou dangereux.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .dockerignore&lt;/span&gt;
venv/
.venv/
__pycache__/
&lt;span class="k"&gt;*&lt;/span&gt;.pyc
.env
.git/
&lt;span class="k"&gt;*&lt;/span&gt;.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;code&gt;.env&lt;/code&gt; dans ce fichier est critique. Sans ça, tes clés secrètes pourraient se retrouver dans l'image Docker publiée sur Docker Hub. Publiquement accessibles.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Étape 5 — Builder l'image
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; mon-api-flask:v1 &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Observe les logs. Chaque step correspond à une instruction du Dockerfile.&lt;/p&gt;

&lt;p&gt;Maintenant fais un second build sans rien changer :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; mon-api-flask:v1 &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tu verras &lt;code&gt;CACHED&lt;/code&gt; sur les steps &lt;code&gt;pip install&lt;/code&gt;. Le cache fonctionne.&lt;/p&gt;

&lt;p&gt;Vérifie l'image créée :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker images
&lt;span class="c"&gt;# REPOSITORY       TAG    SIZE&lt;/span&gt;
&lt;span class="c"&gt;# mon-api-flask    v1     ~180MB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Étape 6 — Lancer le container et tester
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Lance le container&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 5000:5000 &lt;span class="nt"&gt;--name&lt;/span&gt; api-flask mon-api-flask:v1

&lt;span class="c"&gt;# Vérifie qu'il tourne&lt;/span&gt;
docker ps

&lt;span class="c"&gt;# Lis les logs Gunicorn&lt;/span&gt;
docker logs &lt;span class="nt"&gt;-f&lt;/span&gt; api-flask
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ouvre &lt;code&gt;http://localhost:5000&lt;/code&gt; dans ton navigateur. L'API répond.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Décryptage des flags &lt;code&gt;docker run&lt;/code&gt; :&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Signification&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mode détaché — tourne en arrière-plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-p 5000:5000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Port mapping hôte:container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--name api-flask&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nom lisible au lieu d'un hash&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Étape 7 — Sécuriser avec les variables d'environnement
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Crée le fichier de configuration&lt;/span&gt;
nano .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FLASK_ENV=production
SECRET_KEY=change-cette-valeur-en-production
PORT=5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relance le container avec ces variables injectées :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker stop api-flask &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker &lt;span class="nb"&gt;rm &lt;/span&gt;api-flask
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 5000:5000 &lt;span class="nt"&gt;--name&lt;/span&gt; api-flask &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env mon-api-flask:v1

&lt;span class="c"&gt;# Vérifie que les variables sont bien reçues&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;api-flask &lt;span class="nb"&gt;env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Le principe :&lt;/strong&gt; l'image Docker ne contient jamais les valeurs sensibles. Elle les reçoit au moment du lancement. La même image peut tourner en dev, staging et production avec des configurations différentes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Étape 8 — Publier sur Docker Hub
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Connexion&lt;/span&gt;
docker login

&lt;span class="c"&gt;# Tag avec ton pseudo Docker Hub&lt;/span&gt;
docker tag mon-api-flask:v1 tonpseudo/mon-api-flask:v1

&lt;span class="c"&gt;# Push&lt;/span&gt;
docker push tonpseudo/mon-api-flask:v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ton image est maintenant accessible depuis n'importe quel serveur dans le monde :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Sur n'importe quelle machine avec Docker&lt;/span&gt;
docker pull tonpseudo/mon-api-flask:v1
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 5000:5000 tonpseudo/mon-api-flask:v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;C'est ça, la promesse de Docker : &lt;strong&gt;construis une fois, fais tourner partout.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Ce que ce projet t'a appris
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Ce que tu maîtrises maintenant&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Layers Docker&lt;/td&gt;
&lt;td&gt;L'ordre des instructions = stratégie de cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Port mapping&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;-p hôte:container&lt;/code&gt; — sans ça, l'app est invisible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.dockerignore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Protège les secrets, allège le build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Variables d'env&lt;/td&gt;
&lt;td&gt;Séparation config/code — principe 12-factor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gunicorn&lt;/td&gt;
&lt;td&gt;Serveur WSGI de production pour Python&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker Hub&lt;/td&gt;
&lt;td&gt;Registry public — le "GitHub" des images&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Pour aller plus loin
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Docker Compose + PostgreSQL&lt;/strong&gt; — Ajouter une base de données persistante connectée à Flask&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions CI/CD&lt;/strong&gt; — Automatiser build + push à chaque &lt;code&gt;git push&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Déploiement VPS&lt;/strong&gt; — Nginx reverse proxy + HTTPS via Let's Encrypt&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Tu n'as plus besoin de dire &lt;em&gt;"ça marchait chez moi"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Ton app Flask est dans une boîte hermétique. Elle tourne sur ton Ubuntu, sur le serveur du client, sur la machine de ton collègue — de manière identique, sans rien réinstaller.&lt;/p&gt;

&lt;p&gt;C'est exactement ce qu'un client freelance ou un recruteur veut voir : pas juste du code qui tourne en local, mais un artefact déployable et professionnel.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Le projet est sur GitHub et l'image sur Docker Hub — liens en fin d'article.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Roméo DOSSOU — Cloud &amp;amp; DevSecOps Engineer | Ouidah, Bénin&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Je publie des guides pratiques Docker, Kubernetes et Cloud pour les ingénieurs francophones africains.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>python</category>
      <category>flask</category>
      <category>devops</category>
    </item>
    <item>
      <title>Docker Compose en production : les 5 erreurs que personne ne te dit</title>
      <dc:creator>Roméo DOSsOu</dc:creator>
      <pubDate>Thu, 02 Jul 2026 11:51:10 +0000</pubDate>
      <link>https://dev.to/romeo_dossou_b9a9ace7ba7c/docker-compose-en-production-les-5-erreurs-que-personne-ne-te-dit-c2g</link>
      <guid>https://dev.to/romeo_dossou_b9a9ace7ba7c/docker-compose-en-production-les-5-erreurs-que-personne-ne-te-dit-c2g</guid>
      <description>&lt;h1&gt;
  
  
  Docker Compose en production : les 5 erreurs que personne ne te dit
&lt;/h1&gt;

&lt;p&gt;La plupart des tutoriels Docker Compose t'apprennent à faire tourner une app sur ton ordinateur. Mais quand tu passes en production — un vrai serveur, de vrais utilisateurs, de vraies données — tu découvres des pièges que ces tutoriels n'ont jamais mentionnés.&lt;/p&gt;

&lt;p&gt;Cet article ne construit rien from scratch. Il autopsie 5 erreurs réelles, montre ce qui casse, explique pourquoi, et donne la correction exacte. C'est le contenu que les ingénieurs seniors gardent pour eux — et que les juniors paient en nuits blanches pour apprendre.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Niveau requis&lt;/strong&gt; : tu sais ce qu'est Docker Compose et tu as déjà écrit un &lt;code&gt;docker-compose.yml&lt;/code&gt;. Si tu es débutant complet, commence par le &lt;a href="https://dev.tolien_github"&gt;TUTORIAL.md&lt;/a&gt; avant cet article.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Le fichier qui a l'air correct — mais qui contient 5 bombes
&lt;/h2&gt;

&lt;p&gt;Voici un &lt;code&gt;docker-compose.yml&lt;/code&gt; typique. Il a l'air propre. Il fonctionne en local. Chaque ligne a l'air raisonnable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supersecret123&lt;/span&gt;    &lt;span class="c1"&gt;# 💣 Bombe #3&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./postgres-data:/var/lib/postgresql/data&lt;/span&gt;  &lt;span class="c1"&gt;# 💣 Bombe #2&lt;/span&gt;

  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./backend&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;FLASK_DEBUG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;                  &lt;span class="c1"&gt;# 💣 Bombe #3&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;                                 &lt;span class="c1"&gt;# 💣 Bombe #1&lt;/span&gt;

  &lt;span class="na"&gt;nginx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:alpine&lt;/span&gt;                    &lt;span class="c1"&gt;# 💣 Bombe #4&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
                                           &lt;span class="c1"&gt;# 💣 Bombe #5 : absence de restart/logs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Allons désarmer chaque bombe.&lt;/p&gt;




&lt;h2&gt;
  
  
  💣 Erreur #1 — Le health check qui ment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Le symptôme
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;api_1 | OperationalError: could not connect to server
api_1 | Connection refused (host "db", port 5432)
api_1 exited with code 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cette erreur apparaît dans les 30 premières secondes du démarrage. En local : invisible (la DB démarre en 2 secondes). Sur un VPS chargé : dévastateur.&lt;/p&gt;

&lt;h3&gt;
  
  
  La cause racine
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;depends_on: - db&lt;/code&gt; dit à Docker : &lt;em&gt;"attends que le conteneur &lt;code&gt;db&lt;/code&gt; soit démarré"&lt;/em&gt;. Pas qu'il soit prêt à accepter des connexions.&lt;/p&gt;

&lt;p&gt;PostgreSQL démarre en plusieurs phases. Le process démarre (← Docker s'arrête là), puis il initialise les fichiers, charge la config, crée les bases — et seulement à la fin il ouvre le port 5432. Sur un VPS avec peu de RAM, ces étapes peuvent prendre 20-40 secondes.&lt;/p&gt;

&lt;h3&gt;
  
  
  La correction
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15&lt;/span&gt;
  &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;myapp"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
    &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;    &lt;span class="c1"&gt;# ← le paramètre le plus ignoré&lt;/span&gt;

&lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;    &lt;span class="c1"&gt;# ← attend vraiment que DB soit prête&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;start_period&lt;/code&gt;&lt;/strong&gt; : pendant les 30 premières secondes, les échecs ne comptent pas. C'est la grâce au démarrage. Sans lui, Docker marque la DB &lt;code&gt;unhealthy&lt;/code&gt; avant même qu'elle ait eu le temps de s'initialiser.&lt;/p&gt;




&lt;h2&gt;
  
  
  💣 Erreur #2 — Les volumes qui font disparaître les données
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Le symptôme
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down
docker compose up
&lt;span class="c"&gt;# Base de données vide. 3 mois de données clients perdus.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  La cause racine
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./postgres-data:/var/lib/postgresql/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un &lt;strong&gt;bind mount&lt;/strong&gt; monte un dossier de ta machine dans le conteneur. C'est parfait pour du code en développement. Pour une base de données en production : problèmes de permissions, risque de corruption, données liées à un seul serveur.&lt;/p&gt;

&lt;p&gt;La commande la plus dangereuse de Docker, avec une seule lettre de différence :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down      &lt;span class="c"&gt;# ✅ Conteneurs supprimés, données conservées&lt;/span&gt;
docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;   &lt;span class="c"&gt;# ❌ Conteneurs ET volumes supprimés → données perdues&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  La correction
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;    &lt;span class="c1"&gt;# volume nommé&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;    &lt;span class="c1"&gt;# Docker gère l'emplacement&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et séparer dev et prod avec deux fichiers :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.override.yml (dev uniquement, fusionné automatiquement)&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./backend:/app&lt;/span&gt;    &lt;span class="c1"&gt;# live reload uniquement en dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💣 Erreur #3 — Les secrets exposés
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Le symptôme (découvert 48h plus tard)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; .env
git show HASH:.env
&lt;span class="c"&gt;# POSTGRES_PASSWORD=supersecret123&lt;/span&gt;
&lt;span class="c"&gt;# Visible dans l'historique Git. Pour toujours.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Des bots scannent GitHub 24h/24. Délai moyen entre le push et la compromission : moins de 4 heures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Les 3 vecteurs d'exposition
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Vecteur 1&lt;/strong&gt; : le &lt;code&gt;.env&lt;/code&gt; commité dans Git.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vecteur 2&lt;/strong&gt; : un secret passé en &lt;code&gt;ARG&lt;/code&gt; dans le Dockerfile.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;history &lt;/span&gt;mon-image
&lt;span class="c"&gt;# ENV DATABASE_URL=postgresql://admin:supersecret@db/myapp&lt;/span&gt;
&lt;span class="c"&gt;# Gravé dans les layers de l'image. Lisible par quiconque y a accès.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Vecteur 3&lt;/strong&gt; : Flask en mode debug en production — affiche toutes les variables d'environnement dans le navigateur en cas d'erreur.&lt;/p&gt;

&lt;h3&gt;
  
  
  La correction
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .gitignore — la ligne la plus importante de ton repo&lt;/span&gt;
.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml — référencer, jamais écrire en dur&lt;/span&gt;
&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;    &lt;span class="c1"&gt;# lu depuis .env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Vérifier avant de démarrer&lt;/span&gt;
docker compose config
&lt;span class="c"&gt;# Toutes les ${VARIABLES} doivent être remplacées par de vraies valeurs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💣 Erreur #4 — Nginx qui dégrade silencieusement
&lt;/h2&gt;

&lt;p&gt;Ces bugs ne génèrent pas d'erreur 500. Ils génèrent des comportements étranges impossibles à diagnostiquer sans savoir où chercher.&lt;/p&gt;

&lt;h3&gt;
  
  
  Les 4 symptômes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. L'app reçoit toujours &lt;code&gt;127.0.0.1&lt;/code&gt;&lt;/strong&gt; — géolocalisation cassée, rate limiting inutile, logs inutilisables. Cause : headers proxy non configurés.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Uploads échouent au-delà d'1MB&lt;/strong&gt; — valeur par défaut de &lt;code&gt;client_max_body_size&lt;/code&gt;. Non documentée dans les tutos. Découverte en production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. WebSockets coupés toutes les 60 secondes&lt;/strong&gt; — &lt;code&gt;proxy_read_timeout&lt;/code&gt; par défaut : 60s. Pour un chat en temps réel, c'est catastrophique.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Contenu périmé servi aux utilisateurs&lt;/strong&gt; — Nginx cache les réponses API sans qu'on lui demande.&lt;/p&gt;

&lt;h3&gt;
  
  
  La configuration production-ready
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;client_max_body_size&lt;/span&gt; &lt;span class="mi"&gt;50m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;# résout le symptôme #2&lt;/span&gt;
    &lt;span class="kn"&gt;server_tokens&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;# masque la version Nginx&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/api/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://api:5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_read_timeout&lt;/span&gt; &lt;span class="s"&gt;300s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;# résout le symptôme #3&lt;/span&gt;

        &lt;span class="c1"&gt;# Résout le symptôme #1 :&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt;              &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt;         &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;   &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;# Résout le symptôme #4 :&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_no_cache&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;"no-store,&lt;/span&gt; &lt;span class="s"&gt;no-cache,&lt;/span&gt; &lt;span class="s"&gt;must-revalidate"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💣 Erreur #5 — Le crash silencieux
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Le symptôme
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
&lt;span class="c"&gt;# NAME      STATUS              PORTS&lt;/span&gt;
&lt;span class="c"&gt;# db        Up 3 days (healthy) 5432/tcp&lt;/span&gt;
&lt;span class="c"&gt;# nginx     Up 3 days (healthy) 0.0.0.0:80-&amp;gt;80/tcp&lt;/span&gt;
&lt;span class="c"&gt;# api       Exited (1) 2h ago   ←  mort depuis 2 heures&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aucune alerte. Aucun redémarrage. L'app était down depuis 2 heures quand le client a appelé.&lt;/p&gt;

&lt;h3&gt;
  
  
  La correction en 2 parties
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Restart policy :&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;span class="c1"&gt;# Redémarre si crash ✅&lt;/span&gt;
&lt;span class="c1"&gt;# Redémarre au reboot du serveur ✅&lt;/span&gt;
&lt;span class="c1"&gt;# Respecte docker compose stop ✅&lt;/span&gt;
&lt;span class="c1"&gt;# N'entre pas en restart loop ✅&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pourquoi pas &lt;code&gt;always&lt;/code&gt; ? Si ton app crashe en boucle à cause d'un bug, &lt;code&gt;always&lt;/code&gt; redémarre en boucle — le serveur se sature et tu ne peux plus diagnostiquer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logs bornés :&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;logging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json-file"&lt;/span&gt;
  &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;max-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10m"&lt;/span&gt;    &lt;span class="c1"&gt;# 10MB par fichier&lt;/span&gt;
    &lt;span class="na"&gt;max-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5"&lt;/span&gt;      &lt;span class="c1"&gt;# 5 fichiers max = 50MB total&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sans ça, les logs grossissent indéfiniment. Sur un VPS avec 20GB de disque, ils peuvent tout saturer en quelques semaines — et quand le disque est plein, tout crashe.&lt;/p&gt;




&lt;h2&gt;
  
  
  La checklist — à garder ouverte à chaque déploiement
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Zone 1 — Health Checks&lt;/span&gt;
docker compose config | &lt;span class="nb"&gt;grep &lt;/span&gt;healthcheck
docker compose ps    &lt;span class="c"&gt;# → tous "(healthy)"&lt;/span&gt;

&lt;span class="c"&gt;# Zone 2 — Volumes&lt;/span&gt;
docker volume &lt;span class="nb"&gt;ls&lt;/span&gt;     &lt;span class="c"&gt;# → volume nommé présent&lt;/span&gt;

&lt;span class="c"&gt;# Zone 3 — Secrets&lt;/span&gt;
git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; .env    &lt;span class="c"&gt;# → aucun résultat&lt;/span&gt;
docker compose config | &lt;span class="nb"&gt;grep &lt;/span&gt;POSTGRES_PASSWORD    &lt;span class="c"&gt;# → vraie valeur, pas ${VAR}&lt;/span&gt;

&lt;span class="c"&gt;# Zone 4 — Nginx&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;nginx nginx &lt;span class="nt"&gt;-t&lt;/span&gt;    &lt;span class="c"&gt;# → "syntax is ok"&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"X-Real-IP"&lt;/span&gt; nginx.conf   &lt;span class="c"&gt;# → présent&lt;/span&gt;

&lt;span class="c"&gt;# Zone 5 — Restart &amp;amp; Logs&lt;/span&gt;
docker compose config | &lt;span class="nb"&gt;grep &lt;/span&gt;restart    &lt;span class="c"&gt;# → "unless-stopped" partout&lt;/span&gt;
docker compose config | &lt;span class="nb"&gt;grep &lt;/span&gt;max-size   &lt;span class="c"&gt;# → "10m" partout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La checklist complète avec toutes les commandes de vérification est disponible dans le repo GitHub : &lt;a href="https://dev.tolien_github"&gt;PRODUCTION.md&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;La différence entre un environnement de dev et un environnement de production se résume souvent à ces 5 zones. Les corriger toutes d'un coup, dès le départ, c'est exactement ce qui différencie un livrable junior d'un livrable senior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Un Compose qui tourne en local, c'est un prototype.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Un Compose qui passe la checklist, c'est un livrable professionnel.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;📎 &lt;strong&gt;Ressources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo GitHub complet : &lt;a href="https://github.com/Bordley/docker-compose-production-checklist.git" rel="noopener noreferrer"&gt;https://github.com/Bordley/docker-compose-production-checklist.git&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Documentation officielle Docker : &lt;a href="https://docs.docker.com/compose/compose-file/" rel="noopener noreferrer"&gt;docs.docker.com/compose/compose-file&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;12-factor app methodology : &lt;a href="https://12factor.net/fr/" rel="noopener noreferrer"&gt;12factor.net&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Par **Bordley&lt;/em&gt;* — IT Manager &amp;amp; DevOps Engineer, Cotonou, Bénin 🇧🇯*&lt;br&gt;
&lt;em&gt;Contenu DevOps en français pour les ingénieurs africains et les équipes internationales.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>productivity</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Comment travailler en équipe avec Git — Workflow complet pour débutants</title>
      <dc:creator>Roméo DOSsOu</dc:creator>
      <pubDate>Wed, 01 Jul 2026 14:52:41 +0000</pubDate>
      <link>https://dev.to/romeo_dossou_b9a9ace7ba7c/comment-travailler-en-equipe-avec-git-workflow-complet-pour-debutants-1j6d</link>
      <guid>https://dev.to/romeo_dossou_b9a9ace7ba7c/comment-travailler-en-equipe-avec-git-workflow-complet-pour-debutants-1j6d</guid>
      <description>&lt;p&gt;&lt;em&gt;Branches, Pull Requests, code review, conflits : tout ce que personne ne t'a vraiment expliqué.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Quand tu apprends Git seul, tu maîtrises &lt;code&gt;git add&lt;/code&gt;, &lt;code&gt;git commit&lt;/code&gt;, &lt;code&gt;git push&lt;/code&gt;. Puis tu rejoins une équipe et tu réalises que tu ne sais pas vraiment travailler à plusieurs. Les branches te font peur. Les conflits te paralysent. Les Pull Requests t'intimidaient.&lt;/p&gt;

&lt;p&gt;Cet article va changer ça.&lt;/p&gt;

&lt;p&gt;On va simuler une vraie session de travail en équipe : deux développeurs (Alice et Bob) qui implémentent deux features en parallèle, se font reviewer mutuellement, tombent sur un conflit, et le résolvent proprement. Chaque commande est expliquée. Chaque décision est justifiée.&lt;/p&gt;




&lt;h2&gt;
  
  
  Le contexte
&lt;/h2&gt;

&lt;p&gt;On travaille sur une API REST Node.js. L'équipe utilise le &lt;strong&gt;GitHub Flow&lt;/strong&gt; — le workflow le plus répandu dans l'industrie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Principe fondamental du GitHub Flow :&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;main&lt;/code&gt; est toujours stable et déployable&lt;/li&gt;
&lt;li&gt;Chaque feature vit sur sa propre branche&lt;/li&gt;
&lt;li&gt;On merge via une Pull Request reviewée par un pair&lt;/li&gt;
&lt;li&gt;On ne merge que si les tests passent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deux features sont à développer en parallèle :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Alice&lt;/strong&gt; → &lt;code&gt;feature/PROJ-101-user-authentication&lt;/code&gt; (authentification JWT)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bob&lt;/strong&gt; → &lt;code&gt;feature/PROJ-102-user-profile&lt;/code&gt; (profil utilisateur)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Les deux touchent &lt;code&gt;src/routes.js&lt;/code&gt; — le conflit est inévitable. Et c'est voulu.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1 — Poser les fondations
&lt;/h2&gt;

&lt;p&gt;Le lead initialise le projet avec une structure propre et un premier commit :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;api-projet &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;api-projet
git init
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; src/&lt;span class="o"&gt;{&lt;/span&gt;api,middleware,models,services&lt;span class="o"&gt;}&lt;/span&gt; tests
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: initialiser la structure du projet API"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pourquoi ce format de message ?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;chore: initialiser la structure du projet API&lt;/code&gt; suit la convention &lt;strong&gt;Conventional Commits&lt;/strong&gt;. Le format est &lt;code&gt;type(scope): description&lt;/code&gt;. Les types courants :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Usage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;feat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nouvelle fonctionnalité&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fix&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Correction de bug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maintenance, config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ajout/modification de tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;refactor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Refactorisation sans changement de comportement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Un historique de commits bien formaté se lit comme un journal de bord. Dans 6 mois, toi ou un collègue pourrez comprendre l'évolution du projet sans lire une seule ligne de code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 2 — Alice travaille sur l'authentification
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Créer une branche, c'est créer un espace de travail isolé
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch &lt;span class="nt"&gt;-c&lt;/span&gt; feature/PROJ-101-user-authentication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;git switch -c&lt;/code&gt; crée la branche ET bascule dessus. À ce moment, la branche est une &lt;strong&gt;copie exacte de &lt;code&gt;main&lt;/code&gt;&lt;/strong&gt;. Ce que Alice fait ici n'affecte pas &lt;code&gt;main&lt;/code&gt; ni le travail de Bob.&lt;/p&gt;

&lt;h3&gt;
  
  
  Des commits atomiques, pas un gros commit fourre-tout
&lt;/h3&gt;

&lt;p&gt;Alice fait 5 commits distincts — un par responsabilité :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Le middleware JWT&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat(auth): implémenter le middleware JWT"&lt;/span&gt;

&lt;span class="c"&gt;# 2. Le service (logique métier pure)&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat(auth): ajouter le service de génération JWT"&lt;/span&gt;

&lt;span class="c"&gt;# 3. Le controller (pont entre HTTP et service)&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat(auth): ajouter le controller POST /auth/login"&lt;/span&gt;

&lt;span class="c"&gt;# 4. Les routes&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat(auth): enregistrer les routes d'authentification"&lt;/span&gt;

&lt;span class="c"&gt;# 5. Les tests&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"test(auth): ajouter les tests du module authentification"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pourquoi pas tout en un seul commit ?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Parce que si demain le controller a un bug et qu'on doit faire un &lt;code&gt;git revert&lt;/code&gt;, on veut pouvoir annuler &lt;em&gt;seulement&lt;/em&gt; le controller — pas le middleware, pas le service, pas les tests. Des commits atomiques = des rollbacks précis.&lt;/p&gt;

&lt;h3&gt;
  
  
  Une décision sécurité intentionnelle
&lt;/h3&gt;

&lt;p&gt;Dans &lt;code&gt;auth-controller.js&lt;/code&gt;, le message d'erreur est volontairement identique dans les deux cas :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// L'email n'existe pas dans la base&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Identifiants invalides&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Le mot de passe est faux&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Identifiants invalides&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pourquoi ? Si les messages étaient différents ("Email inconnu" vs "Mot de passe incorrect"), un attaquant pourrait tester des milliers d'emails pour savoir lesquels sont enregistrés. On appelle ça l'&lt;strong&gt;énumération de comptes&lt;/strong&gt; — une faille de sécurité classique.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 3 — Bob travaille en parallèle
&lt;/h2&gt;

&lt;h3&gt;
  
  
  L'isolation des branches en pratique
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch main          &lt;span class="c"&gt;# Bob revient sur main&lt;/span&gt;
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; feature/PROJ-102-user-profile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bob part de &lt;code&gt;main&lt;/code&gt; — pas de la branche d'Alice. Il développe &lt;code&gt;GET /users/:id&lt;/code&gt; et modifie aussi &lt;code&gt;src/routes.js&lt;/code&gt; sans le savoir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visualisation de la situation :&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;main          ──●────────────────────────
                 \              \
Alice             ●──●──●──●──●  (routes.js version Alice)
                 \
Bob               ●──●──●        (routes.js version Bob)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Les deux branches ont divergé depuis le même point. Elles vont devoir se rejoindre.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ne jamais exposer les données sensibles
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// user-controller.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;password_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;safeUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;safeUser&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// password_hash n'est jamais envoyé au client&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;C'est du JavaScript destructuring : on extrait &lt;code&gt;password_hash&lt;/code&gt; pour l'écarter, et on retourne tout le reste. Simple et efficace.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 4 — Pull Requests et code review
&lt;/h2&gt;

&lt;p&gt;Alice ouvre la PR #1. Bob la review.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ce qu'une bonne PR contient
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Titre  : feat(auth): implémenter l'authentification JWT (PROJ-101)
Base   : main ← feature/PROJ-101-user-authentication

&lt;span class="gu"&gt;## Ce que fait cette PR&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Middleware de vérification des tokens JWT
&lt;span class="p"&gt;-&lt;/span&gt; Service de génération/vérification
&lt;span class="p"&gt;-&lt;/span&gt; Endpoint POST /auth/login

&lt;span class="gu"&gt;## Comment tester&lt;/span&gt;
npm test -- --testPathPattern=auth

&lt;span class="gu"&gt;## Checklist&lt;/span&gt;
✅ Tests ajoutés | ✅ Pas de secrets hardcodés
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Une bonne description de PR répond à 3 questions : &lt;strong&gt;quoi&lt;/strong&gt; (ce qui change), &lt;strong&gt;pourquoi&lt;/strong&gt; (le contexte), &lt;strong&gt;comment&lt;/strong&gt; (tester).&lt;/p&gt;

&lt;h3&gt;
  
  
  Bob trouve une faille critique
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[SUGGESTION] JWT_SECRET ne devrait pas être optionnel en production.
Si JWT_SECRET n'est pas défini, les tokens sont signés avec
'dev-secret-change-in-prod' — n'importe qui connaissant ce secret
peut forger des tokens valides.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alice corrige immédiatement :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;JWT_SECRET est requis en production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;C'est ça la valeur d'une code review.&lt;/strong&gt; Un deuxième regard voit ce qu'on ne voit plus à force de travailler sur le même code.&lt;/p&gt;

&lt;p&gt;Bob approuve. Alice merge dans &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 5 — Le conflit (ne pas paniquer)
&lt;/h2&gt;

&lt;p&gt;Bob tente de mettre à jour sa branche :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git rebase main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git s'arrête :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CONFLICT (content): Merge conflict in src/routes.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Comprendre les marqueurs de conflit
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;HEAD&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;login&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./api/auth-controller&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;authenticate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./middleware/auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;=======&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getUserById&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./api/user-controller&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c8f3b2a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Entre &lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD&lt;/code&gt; et &lt;code&gt;=======&lt;/code&gt; : &lt;strong&gt;la version de &lt;code&gt;main&lt;/code&gt;&lt;/strong&gt; (le travail d'Alice)&lt;/li&gt;
&lt;li&gt;Entre &lt;code&gt;=======&lt;/code&gt; et &lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt; : &lt;strong&gt;la version de Bob&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Git ne sait pas laquelle choisir — il n'est pas qualifié pour faire ce jugement métier. C'est toujours une décision humaine.&lt;/p&gt;

&lt;h3&gt;
  
  
  La résolution intelligente
&lt;/h3&gt;

&lt;p&gt;Bob ne choisit pas entre Alice et lui — il &lt;strong&gt;combine les deux&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Version finale — combine les deux features&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;login&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./api/auth-controller&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;authenticate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./middleware/auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getUserById&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./api/user-controller&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/auth/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getUserById&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Bob ajoute authenticate !&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/protected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En voyant le middleware d'Alice, Bob réalise que &lt;code&gt;/users/:id&lt;/code&gt; ne devrait pas être public. &lt;strong&gt;Le rebase l'a forcé à prendre une décision de sécurité qu'il aurait pu manquer.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add src/routes.js
git rebase &lt;span class="nt"&gt;--continue&lt;/span&gt;
git push &lt;span class="nt"&gt;--force-with-lease&lt;/span&gt; origin feature/PROJ-102-user-profile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pourquoi &lt;code&gt;--force-with-lease&lt;/code&gt; et pas &lt;code&gt;--force&lt;/code&gt; ?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;--force&lt;/code&gt; écrase la branche distante sans vérification. Si un collègue a poussé quelque chose entre-temps, son travail est perdu.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--force-with-lease&lt;/code&gt; vérifie d'abord que personne n'a poussé depuis ta dernière synchronisation. Si c'est le cas, il refuse et t'alerte. C'est la version respectueuse de &lt;code&gt;--force&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rebase vs Merge — le débat tranché
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Avec merge
* Merge commit
|\
| * commit Bob
| * commit Bob
* Merge pull request d'Alice
|\
| * commit Alice

# Avec rebase
* commit Bob
* commit Bob
* Merge pull request d'Alice
* commit Alice
* commit initial
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le rebase donne un historique &lt;strong&gt;linéaire et lisible&lt;/strong&gt;. On utilise :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;rebase&lt;/code&gt;&lt;/strong&gt; pour mettre à jour sa branche feature depuis &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;merge --no-ff&lt;/code&gt;&lt;/strong&gt; pour intégrer une PR dans &lt;code&gt;main&lt;/code&gt; (conserve la trace de la branche)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Phase 6 — Merge final et leçon sur le scope des PRs
&lt;/h2&gt;

&lt;p&gt;Alice review la PR de Bob et suggère un refactor du router. Bob répond :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Excellente suggestion — je crée un ticket PROJ-115. Cette PR reste focalisée sur le profil utilisateur."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;C'est la bonne réponse.&lt;/strong&gt; Une PR qui gonfle en cours de review :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retarde le merge&lt;/li&gt;
&lt;li&gt;Mélange les responsabilités&lt;/li&gt;
&lt;li&gt;Complique les rollbacks potentiels&lt;/li&gt;
&lt;li&gt;Fatigue le reviewer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Une PR = une feature = un ticket.&lt;/strong&gt; Toujours.&lt;/p&gt;




&lt;h2&gt;
  
  
  L'historique final : une histoire lisible
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*   Merge pull request #2 — feat(users)
|\
| * test(users): tests GET /users/:id
| * feat(users): route GET /users/:id
| * feat(users): controller GET /users/:id
*   Merge pull request #1 — feat(auth)
|\
| * fix(auth): JWT_SECRET obligatoire en prod
| * test(auth): tests authentification
| * feat(auth): routes auth
| * feat(auth): controller login
| * feat(auth): service JWT
| * feat(auth): middleware JWT
|/
* chore: initialiser le projet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;N'importe quel développeur qui rejoint l'équipe peut lire cet historique et comprendre comment le projet a évolué. C'est l'objectif.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ce qu'on a appris
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sur Git :&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Les branches isolent le travail — on ne pollue pas &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Les commits atomiques permettent des rollbacks précis&lt;/li&gt;
&lt;li&gt;Le rebase nettoie l'historique avant de merger&lt;/li&gt;
&lt;li&gt;Les conflits ne sont pas des bugs — ce sont des signaux que deux personnes ont travaillé sur la même zone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sur le travail en équipe :&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Une PR décrit le &lt;em&gt;quoi&lt;/em&gt; et le &lt;em&gt;pourquoi&lt;/em&gt;, pas seulement le &lt;em&gt;comment&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;La code review est collaborative, pas confrontationnelle&lt;/li&gt;
&lt;li&gt;Le scope d'une PR ne grandit pas en cours de review&lt;/li&gt;
&lt;li&gt;Une deuxième paire d'yeux attrape ce qu'on ne voit plus&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sur la sécurité :&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;JWT_SECRET&lt;/code&gt; obligatoire en production&lt;/li&gt;
&lt;li&gt;Messages d'erreur génériques pour éviter l'énumération de comptes&lt;/li&gt;
&lt;li&gt;Ne jamais exposer les données sensibles dans les réponses API&lt;/li&gt;
&lt;li&gt;Les routes avec données personnelles sont toujours protégées&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Le code source complet de ce projet est disponible sur GitHub.&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;N'hésite pas à forker et rejouer les phases toi-même — c'est en faisant qu'on retient.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>git</category>
      <category>softwaredevelopment</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>J'ai conteneurisé une app métier complète en 2h : Docker Compose, Nginx, Flask, PostgreSQL</title>
      <dc:creator>Roméo DOSsOu</dc:creator>
      <pubDate>Wed, 01 Jul 2026 08:28:29 +0000</pubDate>
      <link>https://dev.to/romeo_dossou_b9a9ace7ba7c/jai-conteneurise-une-app-metier-complete-en-2h-docker-compose-nginx-flask-postgresql-4mb</link>
      <guid>https://dev.to/romeo_dossou_b9a9ace7ba7c/jai-conteneurise-une-app-metier-complete-en-2h-docker-compose-nginx-flask-postgresql-4mb</guid>
      <description>&lt;h1&gt;
  
  
  J'ai conteneurisé une app métier complète en 2h — Docker Compose, Nginx, Flask, PostgreSQL
&lt;/h1&gt;

&lt;p&gt;La plupart des tutoriels Docker s'arrêtent à &lt;code&gt;docker run hello-world&lt;/code&gt; ou au conteneur unique.&lt;/p&gt;

&lt;p&gt;En entreprise, une vraie application c'est &lt;strong&gt;plusieurs services qui travaillent ensemble&lt;/strong&gt; : une base de données, un backend, un serveur web. Ce guide montre comment orchestrer tout ça avec Docker Compose, en construisant une app de gestion de stock pour une PME africaine — de zéro à un environnement production-ready.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Ce qu'on va construire
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;taskflow-stock&lt;/strong&gt; : une API REST de gestion de stock et facturation, avec 3 services Docker orchestrés :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INTERNET → NGINX (reverse proxy)
              ↓
           FLASK API (logique métier, Gunicorn)
              ↓
           POSTGRESQL (données persistées)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;À la fin, une seule commande lance tout :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🏗️ Architecture — Pourquoi 3 services séparés ?
&lt;/h2&gt;

&lt;p&gt;Chaque service a &lt;strong&gt;une seule responsabilité&lt;/strong&gt; :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Rôle&lt;/th&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nginx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reçoit le trafic HTTP, redirige vers Flask&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nginx:alpine&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Logique métier, endpoints REST&lt;/td&gt;
&lt;td&gt;Custom (Python 3.12 Alpine)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stockage persistant&lt;/td&gt;
&lt;td&gt;&lt;code&gt;postgres:15-alpine&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Principe clé : moindre exposition.&lt;/strong&gt;&lt;br&gt;
Seul Nginx a un &lt;code&gt;ports:&lt;/code&gt; dans le Compose. Flask et PostgreSQL sont invisibles depuis l'extérieur — accessibles uniquement via le réseau Docker interne &lt;code&gt;app-network&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  📁 Structure du projet
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;taskflow-stock/
├── app/
│   ├── app.py              # Flask + SQLAlchemy
│   ├── requirements.txt
│   ├── Dockerfile          # Multi-stage Alpine
│   └── .dockerignore
├── nginx/
│   └── nginx.conf          # Reverse proxy config
├── postgres/
│   └── init.sql            # Init automatique au 1er démarrage
├── docker-compose.yml
├── .env                    # Secrets (jamais sur Git)
└── .env.example            # Modèle versionnable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🐍 Le backend Flask — Modèles métier contextualisés
&lt;/h2&gt;

&lt;p&gt;J'ai délibérément choisi des entités qui parlent à la réalité des PME africaines :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Produit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;__tablename__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;produits&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;nom&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# "Huile de palme 1L"
&lt;/span&gt;    &lt;span class="n"&gt;reference&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;prix&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c1"&gt;# En FCFA
&lt;/span&gt;    &lt;span class="n"&gt;stock&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relationship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Stock&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;produit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uselist&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;__tablename__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;stocks&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="n"&gt;produit_id&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;produits.id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;quantite&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;seuil_alerte&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Alerte si stock critique
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La connexion à la base ne contient &lt;strong&gt;aucun mot de passe dans le code&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SQLALCHEMY_DATABASE_URI&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tout vient du &lt;code&gt;.env&lt;/code&gt;, jamais du code source.&lt;/p&gt;




&lt;h2&gt;
  
  
  🐳 Le Dockerfile — Multi-stage build
&lt;/h2&gt;

&lt;p&gt;Le secret d'une image légère : séparer la compilation de l'exécution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stage 1 : installation des dépendances (avec outils de compilation)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.12-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; gcc musl-dev postgresql-dev
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;--prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/install &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# Stage 2 : image finale (sans les outils de build)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-alpine&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; libpq
&lt;span class="k"&gt;RUN &lt;/span&gt;adduser &lt;span class="nt"&gt;-D&lt;/span&gt; appuser       &lt;span class="c"&gt;# Utilisateur non-root&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /install /usr/local&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; appuser:appuser /app
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 5000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["gunicorn", "-w", "2", "-b", "0.0.0.0:5000", "app:app"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Résultat : image finale &amp;lt; 90MB, sans compilateur, sans outils inutiles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Astuce cache Docker&lt;/strong&gt; : toujours copier &lt;code&gt;requirements.txt&lt;/code&gt; AVANT le code source. Si les dépendances ne changent pas, Docker saute le &lt;code&gt;pip install&lt;/code&gt; — le build prend 5 secondes au lieu de 2 minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ Nginx — Le reverse proxy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;flask_app&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;# "api" = nom du service Docker, résolu automatiquement&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://flask_app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt;             &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt;        &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;  &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;La magie de &lt;code&gt;api:5000&lt;/code&gt;&lt;/strong&gt; : dans un réseau Docker, chaque service est joignable par son nom. Docker maintient un DNS interne — pas besoin de connaître les IPs, elles changent à chaque recréation de conteneur.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎼 Docker Compose — L'orchestrateur
&lt;/h2&gt;

&lt;p&gt;La partie la plus importante : gérer l'ordre de démarrage correctement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;          &lt;span class="c1"&gt;# Persistance&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_DB}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./app&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DATABASE_URL}&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;   &lt;span class="c1"&gt;# ← attend que db soit VRAIMENT prêt&lt;/span&gt;

  &lt;span class="na"&gt;nginx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:alpine&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;                      &lt;span class="c1"&gt;# ← seul port exposé&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./nginx/nginx.conf:/etc/nginx/nginx.conf:ro&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app-network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;condition: service_healthy&lt;/code&gt; vs &lt;code&gt;depends_on&lt;/code&gt; simple&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;depends_on: - db&lt;/code&gt; : attend que le conteneur soit &lt;em&gt;démarré&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;depends_on: db: condition: service_healthy&lt;/code&gt; : attend que PostgreSQL accepte &lt;em&gt;vraiment&lt;/em&gt; des connexions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La différence élimine 90% des erreurs "connection refused" au démarrage.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Tests de résilience — Ce qui distingue un ingénieur
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Test 1 — Persistance
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down          &lt;span class="c"&gt;# Arrête tout (volumes conservés)&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;         &lt;span class="c"&gt;# Redémarre&lt;/span&gt;
curl http://localhost/api/produits  &lt;span class="c"&gt;# ✅ Données toujours là&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test 2 — Panne partielle
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;kill &lt;/span&gt;api                    &lt;span class="c"&gt;# Simule un crash applicatif&lt;/span&gt;
curl http://localhost/api/produits         &lt;span class="c"&gt;# → 502 (Nginx tient ✅)&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; api                   &lt;span class="c"&gt;# Relance uniquement Flask&lt;/span&gt;
curl http://localhost/api/produits         &lt;span class="c"&gt;# → données de retour ✅&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un &lt;code&gt;502 Bad Gateway&lt;/code&gt; est ici une &lt;strong&gt;bonne nouvelle&lt;/strong&gt; : Nginx reste opérationnel et signale proprement l'indisponibilité du backend, au lieu de crasher lui-même.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Les 5 leçons que ce projet m'a enseignées
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Démarré" ≠ "Prêt"&lt;/strong&gt; — &lt;code&gt;service_healthy&lt;/code&gt; existe pour cette raison&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS interne Docker&lt;/strong&gt; — les noms de services sont des hostnames, pas des IPs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Le moindre port exposé&lt;/strong&gt; — chaque port ouvert est une surface d'attaque&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Les secrets vivent dans &lt;code&gt;.env&lt;/code&gt;&lt;/strong&gt;, jamais dans le code ou l'image&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Un 502 bien géré vaut mieux qu'un timeout silencieux&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🔄 La suite naturelle
&lt;/h2&gt;

&lt;p&gt;Ce projet est la base de 3 extensions :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;VPS + HTTPS&lt;/strong&gt; — déployer sur Ubuntu avec Let's Encrypt&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt; — Prometheus + Grafana en conteneurs supplémentaires&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD&lt;/strong&gt; — GitHub Actions qui déploie automatiquement à chaque push&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  📎 Ressources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🔗 &lt;a href="https://github.com/Bordley/Taskflow-stock.git" rel="noopener noreferrer"&gt;https://github.com/Bordley/Taskflow-stock.git&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📹 &lt;a href="https://youtu.be/cyUCMntb5KA?si=t0-N-sFcKciiGQZb" rel="noopener noreferrer"&gt;https://youtu.be/cyUCMntb5KA?si=t0-N-sFcKciiGQZb&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📚 Doc officielle Docker Compose : docs.docker.com/compose&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Si ce guide t'a aidé, partage-le — et laisse un commentaire avec ta question ou ton propre projet Docker 👇&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bonne conteneurisation !&lt;/strong&gt; 🐳&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Écrit par Bordley — IT Manager &amp;amp; DevOps Engineer, Bénin 🇧🇯&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Je publie des contenus sur le Cloud et DevOps dans le contexte africain.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;code&gt;#docker&lt;/code&gt; &lt;code&gt;#devops&lt;/code&gt; &lt;code&gt;#python&lt;/code&gt; &lt;code&gt;#flask&lt;/code&gt; &lt;code&gt;#postgresql&lt;/code&gt; &lt;code&gt;#nginx&lt;/code&gt; &lt;code&gt;#francophone&lt;/code&gt; &lt;code&gt;#beginners&lt;/code&gt; &lt;code&gt;#tutorial&lt;/code&gt; &lt;code&gt;#africa&lt;/code&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>flask</category>
      <category>postgressql</category>
      <category>nginx</category>
    </item>
    <item>
      <title>J'ai écrit le Dockerfile que TOUT débutant écrit — puis je l'ai détruit erreur par erreur</title>
      <dc:creator>Roméo DOSsOu</dc:creator>
      <pubDate>Fri, 26 Jun 2026 11:25:18 +0000</pubDate>
      <link>https://dev.to/romeo_dossou_b9a9ace7ba7c/jai-ecrit-le-dockerfile-que-tout-debutant-ecrit-puis-je-lai-detruit-erreur-par-erreur-11eb</link>
      <guid>https://dev.to/romeo_dossou_b9a9ace7ba7c/jai-ecrit-le-dockerfile-que-tout-debutant-ecrit-puis-je-lai-detruit-erreur-par-erreur-11eb</guid>
      <description>&lt;h1&gt;
  
  
  J'ai écrit le Dockerfile que TOUT débutant écrit — puis je l'ai détruit erreur par erreur
&lt;/h1&gt;

&lt;p&gt;Il y a quelques semaines, j'ai écrit mon premier vrai Dockerfile "instinctif" — celui que n'importe qui produit en suivant juste sa logique, sans connaître les bonnes pratiques. Et il fonctionnait. L'application démarrait, je voyais ma page web, j'étais content.&lt;/p&gt;

&lt;p&gt;Sauf que ce Dockerfile pesait &lt;strong&gt;1,1 Go&lt;/strong&gt;, exposait une clé API en clair, et faisait tourner mon code avec les pleins pouvoirs administrateur. Sans le savoir.&lt;/p&gt;

&lt;p&gt;Cet article raconte comment je suis passé de ce Dockerfile naïf à une version professionnelle de &lt;strong&gt;90 Mo&lt;/strong&gt;, en corrigeant 7 erreurs une par une — avec, à chaque étape, la preuve concrète du problème avant la solution.&lt;/p&gt;

&lt;p&gt;Si tu n'as jamais touché Docker, tu peux suivre cet article sans aucun prérequis. Si tu en as déjà fait, tu vas probablement reconnaître au moins 3 de ces erreurs dans tes propres projets (je ne juge pas, je les ai toutes faites).&lt;/p&gt;

&lt;h2&gt;
  
  
  Le point de départ : le Dockerfile qu'on écrit tous
&lt;/h2&gt;

&lt;p&gt;Voici, mot pour mot, le genre de Dockerfile qu'on produit naturellement quand on découvre Docker :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; API_KEY=mon_super_secret_123&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "app.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sept lignes. Ça marche. Et ça contient sept problèmes différents. Voyons-les un par un.&lt;/p&gt;

&lt;h2&gt;
  
  
  Erreur n°1 et 2 : l'ordre des instructions détruit le cache
&lt;/h2&gt;

&lt;p&gt;Docker construit une image en couches. Chaque ligne du Dockerfile = une couche. Et Docker met en cache les couches qui n'ont pas changé, pour ne pas refaire un travail inutile.&lt;/p&gt;

&lt;p&gt;Le souci avec &lt;code&gt;COPY . .&lt;/code&gt; placé avant &lt;code&gt;RUN npm install&lt;/code&gt; : si tu modifies &lt;strong&gt;un seul fichier&lt;/strong&gt; de ton code, Docker considère que la couche &lt;code&gt;COPY . .&lt;/code&gt; a changé — et il refait &lt;strong&gt;tout ce qui suit&lt;/strong&gt;, y compris &lt;code&gt;npm install&lt;/code&gt;, qui pourtant n'a rien à voir avec ton changement.&lt;/p&gt;

&lt;p&gt;Résultat concret : 25 à 30 secondes perdues à chaque rebuild, pour rien.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La correction :&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# On copie SEULEMENT les fichiers de dépendances d'abord&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json package-lock.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Le code source, qui change souvent, vient APRÈS&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Après cette correction, modifier ton code source ne déclenche plus jamais un &lt;code&gt;npm install&lt;/code&gt; inutile. La ligne apparaît en cache (&lt;code&gt;CACHED&lt;/code&gt;) à chaque rebuild, tant que tu ne touches pas à &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La règle à retenir&lt;/strong&gt; : dans un Dockerfile, on place ce qui change rarement en haut, et ce qui change souvent en bas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Erreur n°3 : pas de &lt;code&gt;.dockerignore&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Sans ce fichier, l'instruction &lt;code&gt;COPY . .&lt;/code&gt; copie absolument tout ce qui se trouve dans ton dossier de projet — y compris des choses que tu ne voudrais jamais voir dans une image : &lt;code&gt;node_modules&lt;/code&gt; (inutile, il est recréé par &lt;code&gt;npm install&lt;/code&gt;), &lt;code&gt;.git&lt;/code&gt; (tout ton historique de commits), et surtout un éventuel fichier &lt;code&gt;.env&lt;/code&gt; contenant tes mots de passe locaux.&lt;/p&gt;

&lt;p&gt;J'ai testé en créant un faux &lt;code&gt;.env&lt;/code&gt; avec un mot de passe factice, puis j'ai lancé :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; mon-app &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le fichier &lt;code&gt;.env&lt;/code&gt; apparaissait dans la liste. Sans aucune protection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La correction&lt;/strong&gt;, un fichier &lt;code&gt;.dockerignore&lt;/code&gt; classique :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node_modules/
.git/
.env
*.log
tests/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Même logique qu'un &lt;code&gt;.gitignore&lt;/code&gt;, mais pour Docker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Erreur n°4 : la démonstration qui m'a le plus marqué — les secrets immuables
&lt;/h2&gt;

&lt;p&gt;Celle-ci vaut la peine d'être détaillée parce qu'elle m'a vraiment surpris.&lt;/p&gt;

&lt;p&gt;J'ai écrit ceci dans mon Dockerfile :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; API_KEY=mon_super_secret_123&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;unset &lt;/span&gt;API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mon intuition de débutant : "j'ai supprimé la variable juste après, donc elle ne devrait plus être visible". Faux.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;history&lt;/span&gt; &lt;span class="nt"&gt;--no-trunc&lt;/span&gt; mon-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le secret apparaît, en clair, dans l'historique des couches. Pour toujours. Parce que chaque couche Docker est &lt;strong&gt;immuable&lt;/strong&gt; — une fois écrite, elle ne disparaît jamais, même si une couche suivante "annule" son effet visible.&lt;/p&gt;

&lt;p&gt;C'est exactement le genre de vulnérabilité qu'on retrouve régulièrement dans des images publiées par erreur sur des registries publics, avec des clés AWS ou des tokens GitHub encore lisibles dans l'historique.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La correction&lt;/strong&gt; : ne jamais écrire de valeur sensible dans le Dockerfile. On déclare la variable vide, et on injecte sa vraie valeur au moment du &lt;code&gt;docker run&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; API_KEY=""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env.runtime mon-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le fichier &lt;code&gt;.env.runtime&lt;/code&gt; ne quitte jamais ta machine — il n'est jamais copié dans l'image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Erreur n°5 : tourner en root
&lt;/h2&gt;

&lt;p&gt;Par défaut, si tu ne précises rien, ton application tourne avec l'utilisateur &lt;code&gt;root&lt;/code&gt; — l'équivalent administrateur sous Linux. Si quelqu'un trouve une faille dans ton code et l'exploite, il obtient directement les pleins pouvoirs sur le conteneur.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La correction&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /bin/sh appuser
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; appuser:appuser /app
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Après ça, &lt;code&gt;docker run mon-app whoami&lt;/code&gt; répond &lt;code&gt;appuser&lt;/code&gt; au lieu de &lt;code&gt;root&lt;/code&gt;. C'est le principe du moindre privilège — un classique de la sécurité système, appliqué en deux lignes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Erreurs n°6 et 7 : une image dix fois trop lourde
&lt;/h2&gt;

&lt;p&gt;Mon image naïve pesait &lt;strong&gt;1,1 Go&lt;/strong&gt;. Pour comprendre pourquoi, il faut comprendre ce que contient réellement &lt;code&gt;node:20&lt;/code&gt; : un système Debian complet, avec des centaines d'outils dont mon application n'utilise jamais 1%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Première correction&lt;/strong&gt;, changer l'image de base :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alpine est une distribution Linux minimaliste. Ce seul changement m'a fait passer de 1,1 Go à environ 180 Mo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deuxième correction&lt;/strong&gt;, le multi-stage build — la technique la plus puissante de tout ce projet. L'idée : séparer la phase de construction de l'application (qui a besoin de tous les outils) de la phase d'exécution (qui n'a besoin que du résultat final).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stage 1 : on construit, avec tous les outils nécessaires&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json package-lock.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# Stage 2 : on ne garde que le strict nécessaire&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runner&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/app.js ./app.js&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/package.json ./package.json&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "app.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Résultat final : environ &lt;strong&gt;90 Mo&lt;/strong&gt;. Une réduction de 92% par rapport au point de départ.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le bilan complet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Erreur&lt;/th&gt;
&lt;th&gt;Risque&lt;/th&gt;
&lt;th&gt;Gain après correction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mauvais ordre des couches&lt;/td&gt;
&lt;td&gt;Rebuild lent à chaque changement&lt;/td&gt;
&lt;td&gt;30s → 0,1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pas de &lt;code&gt;.dockerignore&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Fichiers sensibles copiés&lt;/td&gt;
&lt;td&gt;Image plus légère et sûre&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets en dur&lt;/td&gt;
&lt;td&gt;Fuite de clés API permanente&lt;/td&gt;
&lt;td&gt;Zéro secret dans l'image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exécution en root&lt;/td&gt;
&lt;td&gt;Accès total en cas de faille&lt;/td&gt;
&lt;td&gt;Utilisateur limité&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image lourde&lt;/td&gt;
&lt;td&gt;1,1 Go pour rien&lt;/td&gt;
&lt;td&gt;-80% avec Alpine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pas de multi-stage&lt;/td&gt;
&lt;td&gt;Outils de build embarqués&lt;/td&gt;
&lt;td&gt;-92% au total&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Ce que j'en retiens
&lt;/h2&gt;

&lt;p&gt;La leçon la plus importante de tout ce projet : &lt;strong&gt;un Dockerfile qui fonctionne et un Dockerfile qui est bien fait sont deux choses complètement différentes&lt;/strong&gt;. La première version naïve ne plante jamais — c'est justement ce qui la rend dangereuse, parce qu'on ne voit aucun signal d'alarme.&lt;/p&gt;

&lt;p&gt;J'ai documenté l'intégralité de ce projet, avec le code source testable des deux versions (avant/après) et un guide pas-à-pas niveau zéro absolu, dans ce dépôt GitHub :&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/Bordley/mon-app.git" rel="noopener noreferrer"&gt;https://github.com/Bordley/mon-app.git&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Si tu repères d'autres erreurs classiques que je n'ai pas couvertes, ou si tu veux discuter d'un point précis, les commentaires sont ouverts.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl1dyuqj9k87v8llgh32w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl1dyuqj9k87v8llgh32w.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>title: "Docker Volumes : pourquoi tes données disparaissent (et comment les protéger pour de bon)"</title>
      <dc:creator>Roméo DOSsOu</dc:creator>
      <pubDate>Thu, 25 Jun 2026 11:39:14 +0000</pubDate>
      <link>https://dev.to/romeo_dossou_b9a9ace7ba7c/title-docker-volumes-pourquoi-tes-donnees-disparaissent-et-comment-les-proteger-pour-de-bon-4bnh</link>
      <guid>https://dev.to/romeo_dossou_b9a9ace7ba7c/title-docker-volumes-pourquoi-tes-donnees-disparaissent-et-comment-les-proteger-pour-de-bon-4bnh</guid>
      <description>&lt;p&gt;published: true&lt;br&gt;
tags: docker, devops, mysql, beginners&lt;br&gt;
cover_image:&lt;br&gt;
canonical_url:&lt;/p&gt;
&lt;h1&gt;
  
  
  Docker Volumes : pourquoi tes données disparaissent (et comment les protéger pour de bon)
&lt;/h1&gt;
&lt;h2&gt;
  
  
  Le piège dans lequel je suis tombé (et toi aussi, probablement)
&lt;/h2&gt;

&lt;p&gt;Voici une expérience que je te conseille de faire toi-même, une seule fois, pour bien comprendre la leçon.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; mysql-demo &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret mysql:8.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tu te connectes, tu crées une table &lt;code&gt;clients&lt;/code&gt;, tu insères 3 lignes. Tu vérifies : tout est là.&lt;/p&gt;

&lt;p&gt;Puis :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; mysql-demo
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; mysql-demo &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret mysql:8.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tu te reconnectes :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- ERROR 1146 (42S02): Table 'shop.clients' doesn't exist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tout a disparu.&lt;/strong&gt; Pas un bug. Pas une erreur de manipulation. C'est exactement comme ça que Docker est censé fonctionner — et c'est précisément pour ça que les &lt;strong&gt;volumes&lt;/strong&gt; existent.&lt;/p&gt;

&lt;p&gt;Dans cet article, je t'explique pourquoi ça arrive, et comment je l'ai résolu avec un projet complet simulant l'infrastructure d'une PME (base de données, fichiers clients, sessions).&lt;/p&gt;




&lt;h2&gt;
  
  
  Pourquoi Docker "oublie" tout par défaut
&lt;/h2&gt;

&lt;p&gt;Un container Docker repose sur un système de fichiers en couches, appelé &lt;strong&gt;OverlayFS&lt;/strong&gt;. Schématiquement :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────┐
│  COUCHE D'ÉCRITURE (container)  │ ← temporaire
├─────────────────────────────────┤
│  COUCHES DE L'IMAGE (lecture    │ ← permanentes
│  seule)                         │
└─────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;L'&lt;strong&gt;image&lt;/strong&gt; (lecture seule) contient le système et les binaires — elle ne change jamais. Quand tu lances un container, Docker ajoute une &lt;strong&gt;couche d'écriture&lt;/strong&gt; par-dessus, où vivent tous les fichiers créés ou modifiés pendant l'exécution.&lt;/p&gt;

&lt;p&gt;Le problème : &lt;code&gt;docker rm&lt;/code&gt; détruit cette couche d'écriture. Et comme rien n'a été écrit ailleurs, tout part avec elle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;L'analogie qui m'a aidé à vraiment comprendre :&lt;/strong&gt; l'image est un livre imprimé. La couche d'écriture est un post-it collé dessus. Tu jettes le post-it → tes notes disparaissent, mais le livre reste intact. Le livre, ici, c'est l'image — jamais affectée par la suppression d'un container.&lt;/p&gt;




&lt;h2&gt;
  
  
  Les 3 solutions, et quand utiliser laquelle
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Named Volume — pour les données critiques
&lt;/h3&gt;

&lt;p&gt;Un named volume est un espace de stockage &lt;strong&gt;entièrement géré par Docker&lt;/strong&gt;. Tu lui donnes un nom, Docker s'occupe du reste.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:8.0&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db_data:/var/lib/mysql&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Quand l'utiliser :&lt;/strong&gt; toute base de données (MySQL, Postgres, MongoDB...). C'est la protection minimale non-négociable pour n'importe quelle donnée que tu ne peux pas te permettre de perdre.&lt;/p&gt;

&lt;p&gt;Preuve que ça fonctionne :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down        &lt;span class="c"&gt;# supprime les containers, PAS le volume&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;       &lt;span class="c"&gt;# les données sont toujours là&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Bind Mount — pour les fichiers accessibles depuis le serveur
&lt;/h3&gt;

&lt;p&gt;Un bind mount connecte un dossier de ta machine hôte directement à un dossier du container. Les deux dossiers sont littéralement &lt;strong&gt;le même&lt;/strong&gt; dossier, vu depuis deux côtés différents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:alpine&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1000:1000"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./uploads:/usr/share/nginx/html/uploads&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Quand l'utiliser :&lt;/strong&gt; fichiers uploadés par les utilisateurs, fichiers de configuration, code source en développement.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Le piège classique :&lt;/strong&gt; un fichier créé depuis l'intérieur du container appartient à &lt;code&gt;root&lt;/code&gt; par défaut. Si ton script de déploiement tourne avec un utilisateur normal, il ne pourra pas le supprimer ou le modifier. La ligne &lt;code&gt;user: "1000:1000"&lt;/code&gt; (ton UID Linux, trouvable avec la commande &lt;code&gt;id&lt;/code&gt;) résout ce problème en alignant les permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. tmpfs — pour les données sensibles et temporaires
&lt;/h3&gt;

&lt;p&gt;Un tmpfs est un espace de stockage qui vit &lt;strong&gt;exclusivement en RAM&lt;/strong&gt;. Il n'est jamais écrit sur disque, et il disparaît automatiquement à l'arrêt du container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tmpfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/tmp/sessions:size=64m,mode=1777&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Quand l'utiliser :&lt;/strong&gt; tokens de session, secrets temporaires, cache haute performance non-critique.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pourquoi c'est important pour la sécurité :&lt;/strong&gt; si un token de session n'existe qu'en RAM, il ne peut pas être lu via un backup disque ou un accès physique malveillant au serveur. C'est le même principe utilisé par des outils comme HashiCorp Vault pour la gestion de secrets.&lt;/p&gt;




&lt;h2&gt;
  
  
  Le tableau de décision
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Besoin&lt;/th&gt;
&lt;th&gt;Type à utiliser&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base de données&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Named Volume&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fichiers uploadés / configuration&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Bind Mount&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code source en dev (hot-reload)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Bind Mount&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessions / secrets temporaires&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;tmpfs&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache rapide non-critique&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;tmpfs&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Et si le volume lui-même est supprimé ? (Backup &amp;amp; Restore)
&lt;/h2&gt;

&lt;p&gt;Un named volume protège contre la suppression du &lt;em&gt;container&lt;/em&gt;. Mais que se passe-t-il si le &lt;em&gt;volume&lt;/em&gt; lui-même est supprimé, ou si le serveur entier est perdu ?&lt;/p&gt;

&lt;p&gt;C'est là qu'intervient la sauvegarde. Comme Docker cache l'emplacement réel d'un named volume, on ne peut pas simplement faire un &lt;code&gt;cp&lt;/code&gt;. La technique standard consiste à utiliser un container Alpine temporaire qui sert de pont entre le volume et un dossier de backup :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; db_data:/data:ro &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/backups:/backups &lt;span class="se"&gt;\&lt;/span&gt;
  alpine &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;tar &lt;/span&gt;czf /backups/db_backup_&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d_%H%M%S&lt;span class="si"&gt;)&lt;/span&gt;.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /data &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et pour restaurer après une suppression totale du volume :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker volume create db_data

docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; db_data:/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/backups:/backups:ro &lt;span class="se"&gt;\&lt;/span&gt;
  alpine &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;tar &lt;/span&gt;xzf /backups/db_backup_20260615.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;J'ai testé cette procédure de bout en bout : suppression volontaire du volume, puis restauration complète. Toutes les données sont revenues, exactement comme avant le sinistre.&lt;/p&gt;




&lt;h2&gt;
  
  
  La stack finale
&lt;/h2&gt;

&lt;p&gt;Le projet complet assemble 3 services représentant une infrastructure réaliste de PME :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App (nginx)&lt;/strong&gt; → Bind Mount pour les uploads + tmpfs pour les sessions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL&lt;/strong&gt; → Named Volume pour la base de données&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt; → Named Volume + tmpfs pour le cache&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avec un healthcheck pour fiabiliser le démarrage, un utilisateur dédié non-root pour la base de données (principe du moindre privilège), et une politique de redémarrage automatique.&lt;/p&gt;

&lt;p&gt;Le code complet est disponible sur GitHub &lt;em&gt;(lien à ajouter)&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ce que j'en retiens
&lt;/h2&gt;

&lt;p&gt;Comprendre les volumes Docker, ce n'est pas mémoriser trois commandes. C'est comprendre que &lt;strong&gt;le cycle de vie d'un container et le cycle de vie d'une donnée sont deux choses complètement différentes&lt;/strong&gt;. Un container est jetable par design — c'est même ce qui rend Docker puissant (scalabilité, mises à jour sans risque, reproductibilité). Une donnée, elle, ne l'est pas.&lt;/p&gt;

&lt;p&gt;Une fois ce modèle mental posé, le choix entre Named Volume, Bind Mount et tmpfs n'est plus une question de mémorisation — c'est une question de bon sens appliqué au bon contexte.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Je documente mon apprentissage du Cloud et du DevOps, avec un focus sur des projets réalistes plutôt que des tutoriels abstraits. Si ce genre de contenu t'intéresse, n'hésite pas à me suivre.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  docker #devops #mysql #beginners #cloudcomputing #infrastructure #containerization #backend #learning #africatech
&lt;/h1&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>mysql</category>
      <category>cloudcomputing</category>
    </item>
    <item>
      <title># J'ai réduit mon image Docker Flask de 445MB à 35MB — voici exactement comment</title>
      <dc:creator>Roméo DOSsOu</dc:creator>
      <pubDate>Mon, 22 Jun 2026 17:19:28 +0000</pubDate>
      <link>https://dev.to/romeo_dossou_b9a9ace7ba7c/-jai-reduit-mon-image-docker-flask-de-445mb-a-35mb-voici-exactement-comment-32fg</link>
      <guid>https://dev.to/romeo_dossou_b9a9ace7ba7c/-jai-reduit-mon-image-docker-flask-de-445mb-a-35mb-voici-exactement-comment-32fg</guid>
      <description>&lt;p&gt;&lt;em&gt;Temps de lecture : 8 minutes — niveau débutant/intermédiaire&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Il y a quelques semaines, je dockerise une petite API Flask. Deux routes, 80 lignes de code. Je lance &lt;code&gt;docker build&lt;/code&gt;, j'attends, et quand je tape &lt;code&gt;docker images&lt;/code&gt; — &lt;strong&gt;445MB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Quatre cent quarante-cinq mégaoctets. Pour une app qui tient dans un fichier.&lt;/p&gt;

&lt;p&gt;Je me suis dit : quelque chose ne va pas. Et j'ai décidé de comprendre &lt;em&gt;exactement&lt;/em&gt; d'où venait ce poids, et comment l'éliminer. Ce que j'ai trouvé a transformé ma façon d'écrire tous mes Dockerfiles depuis.&lt;/p&gt;

&lt;p&gt;Voici la méthode complète, chiffres à l'appui.&lt;/p&gt;




&lt;h2&gt;
  
  
  Le point de départ : le Dockerfile "débutant"
&lt;/h2&gt;

&lt;p&gt;Voici le Dockerfile que j'avais écrit — et que la majorité des tutoriels en ligne t'apprennent à écrire :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 5000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "app.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Propre. Lisible. Fonctionnel. Et catastrophiquement inutile en production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Étape 1 : diagnostiquer avant de toucher quoi que ce soit
&lt;/h2&gt;

&lt;p&gt;Avant d'optimiser, j'ai fait la radiographie. La commande &lt;code&gt;docker history flask-fat&lt;/code&gt; affiche chaque couche de l'image avec son poids :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt    111MB
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .                               37kB&lt;/span&gt;
[ couches de python:3.11 ]             ~900MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le verdict est immédiat : &lt;strong&gt;mon code pèse 37 kilooctets&lt;/strong&gt;. Le reste — 99% du poids — c'est l'infrastructure que j'avais choisie sans y réfléchir.&lt;/p&gt;




&lt;h2&gt;
  
  
  Optimisation 1 : le &lt;code&gt;.dockerignore&lt;/code&gt; (quick win)
&lt;/h2&gt;

&lt;p&gt;Première chose facile : empêcher Docker de copier des fichiers inutiles.&lt;/p&gt;

&lt;p&gt;Sans &lt;code&gt;.dockerignore&lt;/code&gt;, &lt;code&gt;COPY . .&lt;/code&gt; embarque dans l'image : l'environnement virtuel Python local (&lt;code&gt;.venv/&lt;/code&gt; — souvent 200MB+), l'historique Git complet, les fichiers &lt;code&gt;.env&lt;/code&gt; avec les mots de passe...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;.&lt;span class="n"&gt;venv&lt;/span&gt;/
&lt;span class="err"&gt;__&lt;/span&gt;&lt;span class="n"&gt;pycache__&lt;/span&gt;/
.&lt;span class="n"&gt;git&lt;/span&gt;/
.&lt;span class="n"&gt;env&lt;/span&gt;
.&lt;span class="n"&gt;env&lt;/span&gt;.*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ Ce point dépasse la performance : un fichier &lt;code&gt;.env&lt;/code&gt; dans une image publiée sur Docker Hub, c'est tes clés API exposées à tout le monde. C'est l'une des fuites de secrets les plus fréquentes en entreprise.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Gain : marginal sur ce projet, critique en sécurité.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Optimisation 2 : changer l'image de base (-88%)
&lt;/h2&gt;

&lt;p&gt;C'est là que tout se joue. Il existe trois "tailles" d'images Python officielles :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;th&gt;Ce qu'elle contient&lt;/th&gt;
&lt;th&gt;Poids&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python:3.11&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Debian complet + Python + compilateurs + outils&lt;/td&gt;
&lt;td&gt;~900MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python:3.11-slim&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Debian minimal + Python&lt;/td&gt;
&lt;td&gt;~130MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python:3.11-alpine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Alpine Linux + Python (rien d'autre)&lt;/td&gt;
&lt;td&gt;~50MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Un seul mot à changer dans le Dockerfile :&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Avant&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11&lt;/span&gt;

&lt;span class="c"&gt;# Après&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Résultat : &lt;strong&gt;445MB → 53MB&lt;/strong&gt;. Un changement d'une ligne pour -88%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Le piège à connaître&lt;/strong&gt; : Alpine utilise une bibliothèque système différente de Debian (&lt;code&gt;musl libc&lt;/code&gt; vs &lt;code&gt;glibc&lt;/code&gt;). Certaines librairies Python avec des extensions C (&lt;code&gt;psycopg2&lt;/code&gt;, &lt;code&gt;Pillow&lt;/code&gt;, &lt;code&gt;cryptography&lt;/code&gt;) nécessitent des outils de compilation supplémentaires sur Alpine :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; gcc musl-dev libffi-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ce n'est pas compliqué — mais il faut le savoir avant de se retrouver avec des erreurs cryptiques.&lt;/p&gt;




&lt;h2&gt;
  
  
  Optimisation 3 : le multi-stage build
&lt;/h2&gt;

&lt;p&gt;Même avec Alpine, si tu installes des outils de compilation pour certaines librairies, ils restent dans l'image finale. C'est là qu'entre le &lt;strong&gt;multi-stage build&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;L'analogie que j'utilise : imagine une maison en construction. Les maçons arrivent avec échafaudages, bétonnières, perceuses. Quand la maison est prête, tu n'emménages pas avec tout ça — tu livres la maison propre.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stage 1 : on construit ici, avec tous les outils nécessaires&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.11-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; gcc musl-dev libffi-dev
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;--prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/install &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# Stage 2 : image de production, table rase&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.11-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runner&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="c"&gt;# On copie UNIQUEMENT les packages installés — pas gcc, pas musl-dev&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /install /usr/local&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 5000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La ligne magique : &lt;code&gt;COPY --from=builder /install /usr/local&lt;/code&gt;. Elle va chercher uniquement les packages Python compilés dans le stage précédent, sans les outils qui ont servi à les compiler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gain : 53MB → 45MB&lt;/strong&gt; (modeste ici, mais 50-100MB sur des projets avec des dépendances C).&lt;/p&gt;




&lt;h2&gt;
  
  
  Optimisation 4 : épurer les dépendances
&lt;/h2&gt;

&lt;p&gt;Mon &lt;code&gt;requirements.txt&lt;/code&gt; de départ contenait &lt;code&gt;pytest&lt;/code&gt;, &lt;code&gt;black&lt;/code&gt;, &lt;code&gt;ipython&lt;/code&gt;. Ces outils ne servent qu'au développement. En production, ils ne s'exécutent jamais — mais ils pèsent, et surtout &lt;strong&gt;ils représentent des surfaces d'attaque potentielles&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;La solution : deux fichiers distincts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;requirements.txt&lt;/code&gt;&lt;/strong&gt; (production) :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;flask&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=3.0.0&lt;/span&gt;
&lt;span class="py"&gt;gunicorn&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=21.2.0&lt;/span&gt;
&lt;span class="py"&gt;Werkzeug&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=3.0.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;requirements-dev.txt&lt;/code&gt;&lt;/strong&gt; (développement local) :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;-r&lt;/span&gt; &lt;span class="err"&gt;requirements.txt&lt;/span&gt;
&lt;span class="py"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=7.4.0&lt;/span&gt;
&lt;span class="py"&gt;black&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=23.9.1&lt;/span&gt;
&lt;span class="py"&gt;ipython&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=8.16.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le &lt;code&gt;-r requirements.txt&lt;/code&gt; dans le fichier dev signifie "installe d'abord les deps de prod". Un seul fichier à maintenir pour la prod, un seul pour le dev, et zéro duplication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gain : 45MB → 35MB.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Le bilan final
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technique&lt;/th&gt;
&lt;th&gt;Avant&lt;/th&gt;
&lt;th&gt;Après&lt;/th&gt;
&lt;th&gt;Gain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.dockerignore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;445MB&lt;/td&gt;
&lt;td&gt;441MB&lt;/td&gt;
&lt;td&gt;-1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image Alpine&lt;/td&gt;
&lt;td&gt;441MB&lt;/td&gt;
&lt;td&gt;53MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-88%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-stage build&lt;/td&gt;
&lt;td&gt;53MB&lt;/td&gt;
&lt;td&gt;45MB&lt;/td&gt;
&lt;td&gt;-8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dépendances épurées&lt;/td&gt;
&lt;td&gt;45MB&lt;/td&gt;
&lt;td&gt;35MB&lt;/td&gt;
&lt;td&gt;-8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;445MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;35MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-92%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Les 3 règles que j'applique maintenant sur chaque projet
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Commencer par &lt;code&gt;-alpine&lt;/code&gt; ou &lt;code&gt;-slim&lt;/code&gt; par défaut&lt;/strong&gt;, et ne revenir à l'image complète que si une dépendance l'exige vraiment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Multi-stage build systématique&lt;/strong&gt; dès qu'une dépendance nécessite compilation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;.dockerignore&lt;/code&gt; dans chaque projet dès le premier commit&lt;/strong&gt; — pas quand on pense à l'optimisation, dès le début.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ce que ça change concrètement
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Déploiement plus rapide&lt;/strong&gt; : 35MB à transférer au lieu de 445MB. Sur une connexion lente ou un budget bande passante limité (context africain : pertinent), c'est significatif.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Moins de CVEs&lt;/strong&gt; : moins de packages = moins de surface d'attaque = moins de vulnérabilités détectées par les scanners de sécurité comme Trivy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coût réduit&lt;/strong&gt; : sur AWS ECR, Docker Hub, ou tout registry cloud, le stockage et le transfert d'images se facturent au mégaoctet.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Pour aller plus loin
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Le repo GitHub complet avec tous les Dockerfiles (naïf → optimisé) : &lt;a href="https://github.com/Bordley/flask-docker-optimization.git" rel="noopener noreferrer"&gt;https://github.com/Bordley/flask-docker-optimization.git&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Le tutoriel complet étape par étape (niveau zéro débutant) : [TUTORIAL.md dans le repo]&lt;/li&gt;
&lt;li&gt;Prochaine étape : scanner l'image avec &lt;strong&gt;Trivy&lt;/strong&gt; pour mesurer la réduction des CVEs&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Tu travailles sur un projet DevOps ou Cloud ? Je publie régulièrement des contenus techniques en français, avec un angle contexte africain (contraintes réseau, budget VPS, stack locale). Retrouve-moi sur [&lt;a href="http://www.linkedin.com/in/rom%C3%A9o-dossou3777" rel="noopener noreferrer"&gt;www.linkedin.com/in/roméo-dossou3777&lt;/a&gt;] et [Roméo Bordley Tech].&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cloud</category>
      <category>docker</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
