Nous cherchons toujours à construire des services aussi efficaces que possible en termes de ressources. Cela est vrai 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 surveillance, reposent sur des bases de données PostgreSQL. Bien qu’essentielles, ces bases de données restent souvent inactives pendant de longues périodes, consommant mémoire vive et cycles CPU sans raison.
Nous nous sommes donc demandé : 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. Cette conception est incroyablement économe en ressources mais implique quelques compromis, que nous allons explorer.
Voici un aperçu schématique de ce que nous avons construit et de la manière dont nous avons réalisé cette mise à l’échelle dynamique.
flowchart LR
subgraph Machine
A[systemd.socket]
B[systemd-socket-proxyd]
D[(local disk)]
A -- port 15432 --> B
subgraph Docker-Compose
C[postgres container]
end
B -- port 5432 --> C
C -- data-dir --> D
end
X[Internet] -- port 25432 --> A
Le cœur de ce dispositif est l’activation par socket systemd. Au lieu d’avoir un conteneur PostgreSQL fonctionnant 24h/24, nous laissons le système d’initialisation systemd écouter sur le port de la base de données. Quand 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 n’est plus utilisée, elle est automatiquement arrêtée.
Cette approche combine la puissance d’outils Linux classiques éprouvés : systemd pour la gestion des services et l’activation par sockets, et Docker Compose pour définir notre environnement conteneurisé de base de données. C’est simple, robuste, et ne nécessite aucun logiciel personnalisé.
Nous avons fait deux choix technologiques spécifiques 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 dans 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 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 gestion des services par systemd. 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 docker create suivie de docker start est plus difficile à gérer, car systemd nécessiterait un script plus complexe pour gérer ce cycle. docker-compose down offre une commande simple et propre pour la directive ExecStop, garantissant une destruction complète et ordonnée de l’environnement.
Décortiquons 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 trois fichiers d’unité systemd pour gérer le cycle de vie à mise à l’échelle zéro.
C’est un fichier docker-compose.yml standard. Il définit un conteneur PostgreSQL 18, mappe un port interne sur 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 sûres. Tous les réglages documentés dans l’image officielle PostgreSQL sur Docker Hub peuvent être utilisés ici, permettant une personnalisation supplémentaire comme la création d’utilisateurs ou de bases 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. À l’arrivée d’une connexion TCP, systemd activera le service pg1-proxy.service. C’est le point d’entrée pour toutes les connexions à la base.
/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 là que la logique à la demande se trouve. Lorsqu’il est activé par le socket, ce service démarre d’abord le service de base de données réel (Requires=pg1-postgres.service). La commande ExecStartPre est une petite boucle shell critique qui vérifie de manière répétée si le port interne PostgreSQL est ouvert. Sans cette vérification, une condition de compétition pourrait survenir : le proxy démarre et transmet la connexion client avant que le conteneur PostgreSQL ait fini de s’initialiser. Cela provoquerait une erreur immédiate « Connexion refusée » côté client. Ce script pré-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 transfère la connexion entrante vers le port interne sur lequel écoute le conteneur PostgreSQL (127.0.0.1:14532). La part cruciale est --exit-idle-time=3min. Cela indique au proxy de se terminer automatiquement s’il est resté 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. Quand pg1-proxy.service s’arrête (car son minuteur d’inactivité a expiré), systemd considère 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 présente une considération majeure : la latence du « démarrage à froid ». La toute première connexion à la base après une période d’inactivité sera retardée. Le client doit attendre que systemd lance docker-compose up et que le conteneur PostgreSQL s’initialise. Dans notre expérience, cela prend environ une seconde pour une petite base, mais augmente avec la taille du stockage.
Pour de nombreux systèmes internes — CI/CD, travaux batch, ou tableaux de bord d’administration peu utilisés — ce délai est un compromis parfaitement acceptable au regard des importantes économies de ressources. Pour des applications de production à trafic élevé et sensibles à la latence, une base de données traditionnelle, toujours en fonctionnement, reste le choix pertinent.
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 les connexions, mais elle ne consommera aucune ressource tant que la première requête ne sera pas arrivée. C’est une nouvelle petite étape dans notre mission d’élimination des gaspillages, prouvant que même une infrastructure essentielle comme une base relationnelle peut fonctionner de façon légère et à la demande.