← Toutes les fiches

Fiche 13 Parcours : DevOps Lecture : 30 minutes

La mise en production

Livrer sans stress, voir ce qui se passe, et savoir revenir en arrière quand ça tourne mal.

Une mise en production qui fait peur est souvent une mise en production qu'on fait rarement, à la main, sans filet. Le jour où elle casse, personne ne sait vraiment ce qui a changé, ni comment revenir à l'état d'avant, ni si les sauvegardes fonctionnent.

Cette fiche rassemble ce qui rend une mise en production ennuyeuse, au bon sens du terme : une version préparée de la même façon à chaque fois, un déploiement qui ne coupe pas le site, un retour en arrière en une commande, des logs et une surveillance qui disent ce qui se passe, et des sauvegardes qu'on a déjà restaurées. Elle s'appuie sur les fiches 11 (Linux) et 12 (Docker).

À propos des exemples de cette fiche

Les commandes de déploiement, de retour en arrière et de sauvegarde sont des illustrations : aucune n'a été exécutée. La migration sans coupure (en SQL, sur SQLite en mémoire), le log structuré et le cache (en PHP pur) ont été exécutés. Les extraits Symfony et YAML sont des illustrations, non exécutées.

1.Préparer une version de production

Une application Symfony ne se met pas en production telle qu'elle tourne sur ton poste. Elle se construit : sans les outils de développement, avec un cache prêt, des assets compilés. Ces étapes doivent être automatiques et identiques à chaque version, idéalement dans l'intégration continue ou dans le Dockerfile (fiche 12).

exemples/13/build.sh · illustration, non exécuté
# Préparer une version de production (dans l'intégration continue, ou dans le Dockerfile)
export APP_ENV=prod APP_DEBUG=0

composer install --no-dev --optimize-autoloader --no-interaction   # sans outils de dev
composer dump-env prod             # fige les variables .env dans .env.local.php (plus rapide)
php bin/console asset-map:compile  # assets versionnés (AssetMapper), ou ton build front
php bin/console cache:warmup       # le cache Symfony est prêt avant la première requête
  1. Trois environnements au minimum : le développement (ton poste), la recette (staging, une copie de la production où l'on vérifie avant de livrer), et la production. Plus la recette ressemble à la production, plus elle attrape de problèmes.
  2. APP_ENV=prod et APP_DEBUG=0 : sans eux, Symfony est lent et affiche des informations internes (fiche 10).
  3. Une version = un artefact : une archive ou une image Docker, construite une seule fois, puis déployée telle quelle en recette et en production. On ne reconstruit pas différemment pour chaque environnement : ce qui a été vérifié en recette est exactement ce qui part en production.

2.Déployer sans coupure

Copier les nouveaux fichiers par-dessus les anciens, pendant que le site tourne, laisse une fenêtre où une requête mélange l'ancien et le nouveau code. Le principe d'un déploiement propre : préparer la nouvelle version à côté, puis basculer d'un coup.

exemples/13/deploy.sh · illustration, non exécuté
# Déployer une version sur un serveur, sans Docker (étapes d'un outil comme Deployer)
RELEASE=/var/www/shop/releases/2026-11-03-1415      # un dossier neuf par version

# 1. Copier le code construit dans un dossier NEUF (l'ancienne version tourne toujours)
rsync -a build/ "$RELEASE"/
# 2. Relier ce qui doit survivre d'une version à l'autre
ln -s /var/www/shop/shared/.env.local.php "$RELEASE"/.env.local.php
ln -s /var/www/shop/shared/var/uploads   "$RELEASE"/var/uploads
# 3. Migrer la base (des migrations compatibles avec l'ancien code : section 3)
php "$RELEASE"/bin/console doctrine:migrations:migrate --no-interaction
# 4. Basculer d'un coup : le lien « current » pointe vers la nouvelle version
ln -sfn "$RELEASE" /var/www/shop/current
# 5. Recharger PHP-FPM pour vider OPcache
sudo systemctl reload php8.4-fpm
/var/www/shop/
├── current ──► releases/2026-11-03-1415   (le lien que sert nginx)
├── releases/
│   ├── 2026-11-02-0930                     (la version précédente, gardée pour revenir)
│   └── 2026-11-03-1415                     (la nouvelle version)
└── shared/                                 (ce qui survit aux versions)
    ├── .env.local.php
    └── var/uploads/
Déployer souvent

Un déploiement par mois embarque des dizaines de changements : s'il casse, impossible de savoir lequel est en cause. Plusieurs petits déploiements par semaine sont plus sûrs, chacun facile à comprendre et à annuler. Évite en revanche le vendredi soir et la veille de tes congés : il faut être disponible dans les heures qui suivent.

3.Les migrations sans coupure

Pendant le déploiement, l'ancien et le nouveau code tournent parfois en même temps, quelques secondes ou quelques minutes. Une migration qui renomme ou supprime une colonne casse alors l'ancien code. La technique étendre puis contracter (expand and contract) découpe le changement en étapes compatibles :

Renommer une colonne en deux déploiements : étendre, puis contracter Au départ, la table a une colonne name. Au premier déploiement, on ajoute full_name à côté : l'ancien code lit toujours name, le nouveau lit full_name et écrit dans les deux. Au déploiement suivant, quand plus aucun code n'utilise name, on la supprime. Avant customer id email name ① Étendre avec la nouvelle version du code customer id email name full_name (ajoutée) l'ancien code lit name ✓ le nouveau lit full_name ✓ et écrit dans les deux ② Contracter un déploiement plus tard customer id email full_name plus aucun code n'utilise name : on la supprime
Schéma 13.1 · Renommer une colonne en deux déploiements : étendre, puis contracter
exemples/13/expand_contract.sql
-- On veut renommer customer.name en customer.full_name, SANS couper le site.
-- Un RENAME direct casserait l'ancienne version du code,
-- encore en service pendant le déploiement.
CREATE TABLE customer (id INTEGER PRIMARY KEY, email TEXT NOT NULL, name TEXT NOT NULL);
INSERT INTO customer (email, name) VALUES ('alice@example.com', 'Alice Martin');

-- Version 1 du déploiement (ÉTENDRE) : ajouter la nouvelle colonne, facultative
ALTER TABLE customer ADD COLUMN full_name TEXT;

-- … et recopier les données existantes
UPDATE customer SET full_name = name WHERE full_name IS NULL;

-- L'ancien code lit toujours « name » : il fonctionne
SELECT id, name FROM customer;

-- Le nouveau code lit « full_name », et écrit dans les DEUX colonnes pendant la transition
SELECT id, full_name FROM customer;

-- Version 2, plus tard, quand plus aucun code n'utilise « name » (CONTRACTER)
ALTER TABLE customer DROP COLUMN name;

-- La table finale
SELECT id, email, full_name FROM customer;
Sortie
▸ L'ancien code lit toujours « name » : il fonctionne
id | name
---+-------------
1  | Alice Martin
(1 ligne)

▸ Le nouveau code lit « full_name », et écrit dans les DEUX colonnes pendant la transition
id | full_name
---+-------------
1  | Alice Martin
(1 ligne)

▸ La table finale
id | email             | full_name
---+-------------------+-------------
1  | alice@example.com | Alice Martin
(1 ligne)
  1. Étendre : ajouter la nouvelle colonne, facultative, et recopier les données. L'ancien code, qui ignore la colonne, continue de fonctionner.
  2. Déployer le nouveau code, qui lit la nouvelle colonne et écrit dans les deux pendant la transition.
  3. Contracter, lors d'un déploiement suivant : quand plus aucun code n'utilise l'ancienne colonne, la supprimer.

C'est plus long qu'un simple renommage, mais aucune étape ne casse la version en cours. La fiche 8, sur Doctrine, reviendra sur la façon d'écrire ces migrations.

Quand faire plus simple

Si une coupure de quelques minutes est acceptable (une application interne, la nuit), une page de maintenance et une migration directe sont plus simples. Fais ce choix consciemment, et préviens les utilisateurs. Attention aussi aux grosses tables : ajouter un index ou modifier une colonne peut verrouiller la table pendant de longues minutes. Teste la migration sur une copie de la base de production avant.

4.Revenir en arrière

Une erreur 500 sur la page de paiement, cinq minutes après le déploiement. La priorité n'est pas de comprendre le bug : c'est de remettre le site en état. Le retour en arrière (rollback) doit donc être prévu, simple, et déjà essayé.

exemples/13/rollback.sh · illustration, non exécuté
# Revenir à la version précédente du code : un seul lien à changer…
ln -sfn /var/www/shop/releases/2026-11-02-0930 /var/www/shop/current
sudo systemctl reload php8.4-fpm

# … ou, avec Docker et Kubernetes, redéployer l'image précédente
kubectl rollout undo deployment/shop
  1. Le code revient en arrière facilement, à condition d'avoir gardé la version précédente (le dossier, ou l'image avec sa version précise).
  2. La base de données, c'est plus délicat : une migration qui a supprimé une colonne ne se défait pas sans perdre de données. C'est la deuxième raison d'être de la technique de la section 3 : chaque étape laisse la base compatible avec la version précédente du code.
  3. Revenir en arrière ou corriger en avant ? Si le correctif est évident et petit, livrer une nouvelle version corrigée (roll forward) est parfois plus rapide. Mais dans le doute, et en pleine panne, reviens d'abord en arrière, puis prends le temps de comprendre.

5.Les logs

En production, il n'y a ni dump() ni profiler : les logs sont tes yeux. Encore faut-il qu'ils disent quelque chose d'utile, et rien de dangereux.

exemples/13/logging.php
<?php

declare(strict_types=1);

// Ce qu'on reçoit lors d'un paiement refusé
$request = [
    'orderId' => 1042,
    'email' => 'alice@example.com',
    'cardNumber' => '4111111111111111',   // numéro de carte de TEST, fictif
    'password' => 'exemple-fictif',
];

// ✗ Un log en texte libre, qui recopie tout ce qu'on a sous la main
echo 'Paiement refusé pour ', json_encode($request), "\n\n";

// ✓ Un log structuré : un message stable, et seulement le contexte utile, sans donnée sensible
$record = [
    'datetime' => '2026-11-03T14:15:02+01:00',
    'level' => 'warning',
    'message' => 'Payment declined',
    'context' => [
        'order_id' => $request['orderId'],
        'card_last4' => substr($request['cardNumber'], -4),
        'reason' => 'insufficient_funds',
    ],
];
echo json_encode($record, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), "\n";
Sortie
Paiement refusé pour {"orderId":1042,"email":"alice@example.com","cardNumber":"4111111111111111","password":"exemple-fictif"}

{
    "datetime": "2026-11-03T14:15:02+01:00",
    "level": "warning",
    "message": "Payment declined",
    "context": {
        "order_id": 1042,
        "card_last4": "1111",
        "reason": "insufficient_funds"
    }
}
  1. Un message stable, avec les détails dans le contexte : « Payment declined » est le même pour tous les paiements refusés, ce qui permet de les compter et de les chercher. Monolog sépare naturellement le message et le contexte.
  2. Le bon niveau : debug et info pour suivre le fonctionnement normal, warning pour l'inhabituel, error pour un échec à traiter, critical pour ce qui doit réveiller quelqu'un.
  3. Un format structuré (une ligne JSON par événement), envoyé sur la sortie d'erreur. Docker, systemd ou l'hébergeur le récupèrent, et un outil de centralisation permet de chercher dans les logs de tous les serveurs à la fois.
exemples/13/monolog.yaml · illustration, non exécuté
# config/packages/prod/monolog.yaml (extrait)
monolog:
    handlers:
        main:
            type: fingers_crossed      # garde les logs en mémoire…
            action_level: error        # … et ne les écrit que si une erreur survient
            handler: nested
            excluded_http_codes: [404, 405]
        nested:
            type: stream
            path: php://stderr         # la sortie d'erreur : récupérée par Docker ou systemd
            level: debug
            formatter: monolog.formatter.json   # une ligne JSON par événement

Le gestionnaire fingers_crossed garde les messages détaillés en mémoire, et ne les écrit que si une erreur survient pendant la requête : on a tout le contexte des requêtes qui échouent, sans noyer les logs sous les requêtes qui réussissent.

Sécurité : ce qui ne doit jamais apparaître dans un log

Mots de passe, jetons, clés d'API, numéros de carte complets, cookies de session (fiche 9). Les logs sont lus par plus de monde que la base de données, gardés longtemps, copiés dans d'autres outils. Limite aussi les données personnelles (adresses e-mail, adresses postales) au strict nécessaire : le RGPD s'applique aussi aux logs. Un identifiant (order_id) suffit presque toujours pour retrouver le reste en base.

6.La surveillance

Le pire moyen d'apprendre une panne, c'est par un client. La surveillance (monitoring) consiste à savoir qu'il y a un problème avant les utilisateurs, ou au moins en même temps.

  1. Une route de santé que l'on peut appeler à tout moment : le répartiteur de charge, Kubernetes (fiche 12) et l'outil de surveillance l'utilisent.
exemples/13/HealthController.php · illustration, non exécuté
<?php

declare(strict_types=1);

namespace App\Controller;

use Doctrine\DBAL\Connection;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

// Une route légère, appelée par la surveillance, le répartiteur de charge ou Kubernetes
final class HealthController
{
    #[Route('/health', methods: ['GET'])]
    public function __invoke(Connection $connection): JsonResponse
    {
        try {
            $connection->executeQuery('SELECT 1');   // la base répond-elle ?
        } catch (\Throwable) {
            return new JsonResponse(['status' => 'error'], Response::HTTP_SERVICE_UNAVAILABLE);
        }

        // Aucune information interne (version, configuration) : la route est publique
        return new JsonResponse(['status' => 'ok']);
    }
}
  1. Une surveillance depuis l'extérieur : un service tiers appelle ton site toutes les minutes, depuis Internet. Si ton serveur entier est tombé, il ne peut pas te prévenir lui-même.
  2. Quatre indicateurs résument la santé d'une application web : la latence (le temps de réponse), le trafic (le nombre de requêtes), les erreurs (le taux de réponses 5xx), et la saturation (CPU, mémoire, disque, connexions à la base).
  3. Un suivi des erreurs : un outil qui regroupe les exceptions identiques, avec leur trace et leur contexte, et signale les nouvelles après un déploiement.
  4. Des alertes utiles : une alerte doit demander une action. Une alerte qui sonne pour rien dix fois par jour finit ignorée, et la vraie passe inaperçue.
Commencer petit

Pas besoin d'un tableau de bord de quarante graphiques pour commencer. Une route de santé surveillée de l'extérieur, une alerte sur les erreurs 5xx, et une alerte sur l'espace disque couvrent déjà la majorité des pannes. Le reste s'ajoute quand un incident montre ce qui manquait.

7.Sauvegarde et restauration

Une sauvegarde qu'on n'a jamais restaurée n'est pas une sauvegarde : c'est un espoir. Le jour où on en a besoin, on découvre qu'elle est vide, incomplète, ou que personne ne sait la restaurer.

exemples/13/backup.sh · illustration, non exécuté
# Sauvegarder : un fichier au format « custom » de PostgreSQL, compressé
pg_dump --format=custom --file=/backups/shop-2026-11-03.dump shop

# Copier hors du serveur, chiffré (le serveur peut disparaître avec ses sauvegardes)
# … avec l'outil de stockage de ton hébergeur, ou rsync vers une autre machine

# RESTAURER, régulièrement, dans une base de TEST : sinon on ne sait pas si ça marche
createdb shop_restore_test
pg_restore --dbname=shop_restore_test --no-owner /backups/shop-2026-11-03.dump
# Les données sont-elles là ?
psql shop_restore_test -c 'SELECT count(*) FROM customer_order;'
La règle 3-2-1, et le test de restauration La base de production est sauvegardée sur un autre support, puis copiée hors du site, chiffrée et non modifiable : trois copies, sur deux supports, dont une hors site. La sauvegarde est régulièrement restaurée dans une base de test, pour vérifier qu'elle fonctionne. Base de production copie 1 Sauvegarde copie 2, autre support Hors site copie 3, chiffrée Base de test restauration régulière pg_dump copie pg_restore : on vérifie ! 3 copies 2 supports · 1 hors site
Schéma 13.2 · La règle 3-2-1, et le test de restauration
  1. La règle 3-2-1 : trois copies des données, sur deux supports différents, dont une hors du site. Une sauvegarde sur le même serveur que la base disparaît avec lui.
  2. Deux questions à poser au métier, avant la panne :
    • le RPO (Recovery Point Objective) : combien de données acceptons-nous de perdre ? Une sauvegarde chaque nuit, c'est jusqu'à 24 heures de commandes perdues ;
    • le RTO (Recovery Time Objective) : combien de temps de panne acceptons-nous ? Restaurer une base de 50 Go prend du temps, à mesurer à l'avance.
  3. Teste la restauration régulièrement, dans une base séparée, et vérifie que les données sont bien là. Note le temps que ça prend, et écris la procédure : le jour J, il faudra la suivre sous stress.
  4. N'oublie pas les fichiers : les documents envoyés par les utilisateurs (var/uploads, ou le stockage objet) ne sont pas dans la base.
Sécurité : une sauvegarde est une copie complète de tes données

Elle contient tout ce que contient la base : comptes, données personnelles, parfois des secrets. Chiffre-la, limite qui peut y accéder, et supprime les anciennes selon une durée de conservation définie. Garde aussi au moins une copie qu'un attaquant ayant pris le contrôle du serveur ne peut ni modifier ni supprimer : c'est la seule protection contre un rançongiciel.

8.Le cache

Un cache garde le résultat d'un travail coûteux pour le resservir sans le refaire. C'est la mémoïsation de la fiche 1, à l'échelle d'une application. Il en existe plusieurs niveaux :

NiveauCe qu'il gardeExemple
Navigateur et CDNDes réponses HTTP entièresCache-Control, ETag
Reverse proxyDes pages entières, pour tous les utilisateursLe cache HTTP de Symfony, Varnish
ApplicationLe résultat d'un calcul ou d'une requêteLe composant Cache de Symfony, avec Redis
PHPLe code PHP déjà compiléOPcache
Les niveaux de cache, du navigateur à la base de données Une réponse peut venir du cache du navigateur, d'un CDN, du cache HTTP du reverse proxy, du cache applicatif de Symfony, ou en dernier recours de la base de données. Plus elle vient de loin, plus elle coûte cher. Le CDN et le reverse proxy sont partagés entre tous les visiteurs : ils ne doivent jamais garder de données personnelles. son propre cache Navigateur copie proche CDN cache HTTP Reverse proxy Redis, OPcache Symfony Base de données partagés entre tous les visiteurs : jamais de données personnelles plus la réponse vient de loin, plus elle coûte cher
Schéma 13.3 · Les niveaux de cache, du navigateur à la base de données

Le principe et son piège principal, en PHP pur :

exemples/13/cache_aside.php
<?php

declare(strict_types=1);

// Un cache minimal avec durée de vie, et une horloge qu'on avance à la main (fiche 3)
final class TtlCache
{
    private array $items = [];

    public function __construct(private int $now = 0) {}

    public function advance(int $seconds): void
    {
        $this->now += $seconds;
    }

    // « cache-aside » : on regarde dans le cache, sinon on calcule et on range
    public function get(string $key, int $ttl, callable $compute): mixed
    {
        $item = $this->items[$key] ?? null;
        if ($item !== null && $item['expiresAt'] > $this->now) {
            echo "  [cache] $key trouvé\n";

            return $item['value'];
        }
        echo "  [calcul] $key recalculé\n";
        $value = $compute();
        $this->items[$key] = ['value' => $value, 'expiresAt' => $this->now + $ttl];

        return $value;
    }

    public function delete(string $key): void
    {
        unset($this->items[$key]);
    }
}

$prices = ['clavier' => 4990];   // la « base de données »
$cache = new TtlCache();

// use (&$prices) : la fonction lit la base AU MOMENT du calcul, pas une copie figée
$showPrice = function () use ($cache, &$prices): void {
    $price = $cache->get('price_clavier', 3600, function () use (&$prices): int {
        return $prices['clavier'];
    });
    echo "  prix affiché : $price\n";
};

echo "Premier affichage :\n";
$showPrice();
echo "Deuxième affichage :\n";
$showPrice();

echo "Le prix passe à 3990 en base, sans invalider le cache :\n";
$prices['clavier'] = 3990;
$showPrice();

echo "On invalide le cache au moment de la modification :\n";
$cache->delete('price_clavier');
$showPrice();

echo "Une heure plus tard, l'entrée a expiré d'elle-même :\n";
$cache->advance(3601);
$showPrice();
Sortie
Premier affichage :
  [calcul] price_clavier recalculé
  prix affiché : 4990
Deuxième affichage :
  [cache] price_clavier trouvé
  prix affiché : 4990
Le prix passe à 3990 en base, sans invalider le cache :
  [cache] price_clavier trouvé
  prix affiché : 4990
On invalide le cache au moment de la modification :
  [calcul] price_clavier recalculé
  prix affiché : 3990
Une heure plus tard, l'entrée a expiré d'elle-même :
  [calcul] price_clavier recalculé
  prix affiché : 3990

Le troisième affichage montre l'ancien prix : la donnée a changé, et le cache ne le sait pas. Il y a deux façons de limiter ce problème : une durée de vie (au pire, la donnée est périmée pendant une heure), et l'invalidation (supprimer l'entrée au moment où la donnée change). Dans Symfony :

exemples/13/symfony_cache.php · illustration, non exécuté
<?php

use Symfony\Component\HttpKernel\Attribute\Cache;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;

final class CatalogController extends AbstractController
{
    // Cache HTTP : la page publique peut être gardée 10 minutes par les proxys et les CDN
    #[Cache(public: true, maxage: 600)]
    #[Route('/catalogue')]
    public function index(TagAwareCacheInterface $cache, ProductRepository $products): Response
    {
        // Cache applicatif : le résultat d'un calcul coûteux, avec une étiquette
        $compute = function (ItemInterface $item) use ($products): array {
            $item->expiresAfter(3600);
            $item->tag(['products']);

            return $products->findBestSellers(10);
        };
        $bestSellers = $cache->get('best_sellers', $compute);

        return $this->render('catalog/index.html.twig', ['bestSellers' => $bestSellers]);
    }
}

// Quand un produit change : invalider tout ce qui porte l'étiquette « products »
// $cache->invalidateTags(['products']);
exemples/13/opcache.ini · illustration, non exécuté
; docker/php/prod.ini : OPcache garde le code PHP compilé en mémoire
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
; Ne plus vérifier si les fichiers ont changé : plus rapide, mais il faut
; recharger PHP-FPM (ou redémarrer le conteneur) à chaque déploiement
opcache.validate_timestamps=0
Quand ne pas mettre en cache

Le cache ajoute un état de plus à comprendre, et des bugs difficiles (« chez moi c'est à jour, chez lui non »). Ne cache que ce que tu as mesuré comme lent (fiche 1), pas « au cas où ». Et si une requête est lente, regarde d'abord ses index (fiche 7) : un cache masque le problème au lieu de le corriger.

Sécurité : le cache partagé

Une page qui contient des données personnelles (le panier, le nom de l'utilisateur, une facture) ne doit jamais être mise dans un cache partagé (public) : le visiteur suivant verrait les données du précédent. Symfony rend privées par défaut les réponses qui utilisent la session. Et une clé de cache construite à partir d'un en-tête que l'utilisateur contrôle peut permettre d'empoisonner le cache pour tout le monde.

Pièges fréquents

PiègeCe qui arriveLa parade
Un déploiement fait à la main, différemment à chaque foisUne étape oubliée, une panne difficile à comprendreUn script ou un pipeline, toujours le même
Copier les fichiers par-dessus la version en coursDes requêtes servies par un mélange d'ancien et de nouveau codeUne nouvelle version à côté, puis bascule
Renommer ou supprimer une colonne en un seul déploiementL'ancien code plante pendant la transition, retour en arrière impossibleÉtendre, puis contracter
Ne pas garder la version précédenteAucun retour en arrière rapideDossiers de versions, ou images versionnées
Un log qui recopie toute la requêteMots de passe et cartes dans les logsUn message stable, un contexte choisi
Pas de surveillance extérieureLa panne est signalée par un clientUne route de santé surveillée depuis Internet
Des alertes qui sonnent pour rienLa vraie alerte est ignoréeUne alerte = une action à faire
Des sauvegardes jamais restauréesDécouvertes inutilisables le jour JUn test de restauration régulier
La sauvegarde sur le même serveurPerdue avec luiLa règle 3-2-1
Une page personnelle en cache publicUn utilisateur voit les données d'un autreCache privé, ou pas de cache
Oublier de recharger PHP-FPM avec validate_timestamps=0L'ancien code continue de tournerLe rechargement dans le script de déploiement

Checklist avant une mise en production

Avant

Après

Conseils

  1. Automatise un peu à chaque fois. Chaque étape faite à la main est une étape qu'on oubliera un jour. Commence par un script, il deviendra un pipeline.
  2. Répète le retour en arrière et la restauration à froid, un jour calme. En pleine panne, on n'apprend pas : on applique ce qu'on connaît.
  3. Écris un court compte rendu après chaque incident : ce qui s'est passé, comment on l'a vu, ce qu'on change pour que ça ne se reproduise pas. Sans chercher de coupable : le but est d'améliorer le système, pas de juger quelqu'un.
  4. Garde une page de procédures à jour : comment déployer, revenir en arrière, restaurer, où sont les logs. La personne de garde à 3 heures du matin, ce sera peut-être toi.
  5. N'aie pas honte d'une panne. Toutes les équipes en ont. Ce qui compte, c'est le temps qu'il faut pour la voir, la réparer, et en tirer une leçon.

Documentation officielle