Nous cherchons toujours à concevoir des services aussi économes en ressources que possible. Cela vaut pour les services que nous proposons à l’extérieur et, tout aussi important, pour notre propre infrastructure interne. Beaucoup de nos outils internes, comme les systèmes de facturation et de supervision, reposent sur des bases de données PostgreSQL. Bien qu’essentielles, ces bases restent souvent inactives pendant de longues périodes, consommant de la RAM et des cycles CPU inutilement.
Alors nous nous sommes posé la question : pouvons-nous appliquer notre philosophie de mise à l’échelle à zéro à nos propres bases de données ? La réponse est oui. Nous avons développé un système pour provisionner des instances PostgreSQL qui ne fonctionnent que lorsqu’elles sont activement utilisées. Ce design est incroyablement économe en ressources mais comporte certains compromis, que nous explorerons.
Voici un aperçu schématique de ce que nous avons construit et de la façon dont nous avons réalisé cette mise à l’échelle dynamique.
flowchart LR
subgraph Machine
A[systemd.socket]
B[systemd-socket-proxyd]
D[(disque local)]
A -- port 15432 --> B
subgraph Docker-Compose
C[conteneur postgres]
end
B -- port 5432 --> C
C -- data-dir --> D
end
X[Internet] -- port 25432 --> A
Le cœur de cette configuration est l’activation de socket systemd. Au lieu d’avoir un conteneur PostgreSQL en fonctionnement 24/7, nous laissons le système d’init systemd écouter sur le port de la base de données. Lorsqu’une application tente de se connecter, systemd intercepte la requête, démarre le conteneur de base de données à la demande, puis transfère la connexion. Une fois que la base de données n’est plus utilisée, elle est arrêtée automatiquement.
Cette approche combine la puissance d’outils Linux standards et éprouvés : systemd pour la gestion des services et l’activation de socket, et Docker Compose pour définir notre environnement de base de données conteneurisé. C’est simple, robuste et ne nécessite aucun logiciel personnalisé.
Nous avons fait deux choix technologiques précis pour cette configuration : exécuter PostgreSQL dans un conteneur et le gérer avec Docker Compose.
Découplage du système hôte : En exécutant PostgreSQL à l’intérieur d’un conteneur Docker, nous découplons la version de la base de données de celle du système d’exploitation hôte. Cela nous donne la flexibilité d’exécuter différentes versions de PostgreSQL pour différents services internes sur le même hôte sans conflits ni problèmes de dépendances. Nous pouvons mettre à jour une base de données pour un service sans impacter les autres.
Compatibilité avec systemd : Nous avons choisi Docker Compose parce que ses commandes de cycle de vie s’intègrent parfaitement à la façon dont systemd gère les services. La directive ExecStart de systemd attend une commande qui s’exécute au premier plan jusqu’à l’arrêt du service. docker-compose up fait exactement cela. Une sémantique plus classique comme docker create suivie de docker start est plus difficile à gérer, car systemd aurait besoin d’un script plus complexe pour gérer le cycle de vie. docker-compose down fournit une commande unique et propre pour la directive ExecStop, garantissant que l’environnement entier est démonté proprement.
Décomposons maintenant les fichiers de configuration qui rendent cela possible.
Nous utilisons une combinaison d’un fichier docker-compose.yml pour définir la base de données et de trois fichiers d’unités systemd pour gérer le cycle de vie mise-à-zéro.
Ceci est un fichier standard docker-compose.yml. Il définit un conteneur PostgreSQL 18, mappe un port interne vers l’hôte et monte un volume pour persister les données de la base sur le disque local. Cela garantit que même si le conteneur s’arrête, les données restent en sécurité. Tous les paramètres documentés dans l’image PostgreSQL officielle sur Docker Hub peuvent être utilisés ici, permettant une personnalisation supplémentaire comme la création d’utilisateurs ou de bases de données spécifiques au démarrage.
/root/pg/pg1/docker-compose.yml
1version: "3"
2services:
3 database:
4 image: 'postgres:18'
5 ports:
6 - 127.0.0.1:14532:5432
7 volumes:
8 - /root/pg/pg1/data:/var/lib/postgresql
9 environment:
10 POSTGRES_PASSWORD: SuperSecretAdminPassword
Cette unité .socket indique à systemd d’écouter sur le port 24532 sur toutes les interfaces réseau. Lorsqu’une connexion TCP arrive, systemd activera pg1-proxy.service. C’est le point d’entrée pour toutes les connexions à la base de données.
/etc/systemd/system/pg1-proxy.socket
1[Unit]
2Description=Socket for pg1 pg proxy (24532->127.0.0.1:14532)
3
4[Socket]
5ListenStream=0.0.0.0:24532
6ReusePort=true
7NoDelay=true
8Backlog=128
9
10[Install]
11WantedBy=sockets.target
C’est ici que vit la logique à la demande. Lorsqu’elle est activée par le socket, ce service démarre d’abord le service réel de base de données (Requires=pg1-postgres.service). La commande ExecStartPre est une boucle shell petite mais critique qui vérifie de manière répétée si le port PostgreSQL interne est ouvert. Sans cette vérification, une condition de course pourrait survenir où le proxy démarre et redirige la connexion du client avant que le conteneur PostgreSQL n’ait fini son initialisation. Cela se traduirait par une erreur immédiate “Connection Refused” pour le client. Ce script exécuté avant le démarrage assure une transition fluide et que le client ne se connecte qu’une fois la base prête.
Le processus principal est systemd-socket-proxyd, un outil intégré qui transfert la connexion entrante vers le port interne où le conteneur PostgreSQL écoute (127.0.0.1:14532). La partie cruciale est --exit-idle-time=3min. Cela indique au proxy de quitter automatiquement s’il est inactif pendant trois minutes.
/etc/systemd/system/pg1-proxy.service
1[Unit]
2Description=Socket-activated TCP proxy to local Postgres on 14532
3
4Requires=pg1-postgres.service
5After=pg1-postgres.service
6
7[Service]
8Type=simple
9Sockets=pg1-proxy.socket
10ExecStartPre=/bin/bash -c 'for i in {1..10}; do nc -z 127.0.0.1 14532 && exit 0; sleep 1; done; exit 0'
11ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time=3min 127.0.0.1:14532
Ce service gère le cycle de vie Docker Compose. Il est démarré par le service proxy. La directive clé est StopWhenUnneeded=true. Cela lie son cycle de vie à celui du service proxy. Lorsque pg1-proxy.service s’arrête (parce que son temporisateur d’inactivité a expiré), systemd voit que ce service n’est plus nécessaire et l’arrête automatiquement en exécutant docker-compose down. Le conteneur est arrêté, libérant toutes ses ressources.
/etc/systemd/system/pg1-postgres.service
1[Unit]
2Description=postgres container
3PartOf=pg1-proxy.service
4StopWhenUnneeded=true
5
6[Service]
7WorkingDirectory=/root/pg/pg1
8
9Type=simple
10ExecStart=/usr/bin/docker-compose up
11ExecStop=/usr/bin/docker-compose down
12
13Restart=on-failure
14RestartSec=2s
15TimeoutStopSec=30s
Cette configuration est incroyablement efficace, mais elle implique une considération majeure : la latence des “démarrages à froid”. La toute première connexion à la base après une période d’inactivité sera retardée. Le client doit attendre que systemd exécute docker-compose up et que le conteneur PostgreSQL s’initialise. D’après notre expérience, cela prend environ une seconde pour une petite base, mais augmente avec la taille du stockage.
Pour beaucoup de systèmes internes — CI/CD, tâches par lots, ou tableaux de bord d’administration peu utilisés — ce délai est un compromis parfaitement acceptable au regard des économies de ressources significatives. Pour des applications de production à fort trafic et sensibles à la latence, une base de données traditionnelle toujours active reste le bon choix.
Pour mettre une nouvelle base en ligne, il suffit d’activer les unités systemd.
1systemctl daemon-reload
2systemctl enable pg1-proxy.service
3systemctl enable pg1-postgres.service
4systemctl enable --now pg1-proxy.socket
Une fois activée, la base est prête à accepter des connexions, mais elle ne consommera aucune ressource jusqu’à la première connexion. C’est une autre petite étape dans notre mission d’éliminer le gaspillage, démontrant que même une infrastructure essentielle comme une base de données relationnelle peut fonctionner de manière maigre et à la demande.