Wir streben immer danach, Dienste so ressourceneffizient wie möglich zu gestalten. Das gilt sowohl für die von uns extern angebotenen Dienste als auch – ebenso wichtig – für unsere interne Infrastruktur. Viele unserer internen Tools, wie Abrechnungs- und Überwachungssysteme, basieren auf PostgreSQL-Datenbanken. Obwohl sie unverzichtbar sind, stehen diese Datenbanken oft lange Zeit ungenutzt still und verbrauchen dabei unnötig RAM und CPU-Zyklen.
Also haben wir uns gefragt: Können wir unsere Scale-to-Zero-Philosophie auch auf unsere eigenen Datenbanken anwenden? Die Antwort lautet ja. Wir haben ein System entwickelt, das PostgreSQL-Instanzen bereitstellt, die nur laufen, wenn sie aktiv genutzt werden. Dieses Design ist unglaublich ressourceneffizient, bringt aber auch einige Kompromisse mit sich, die wir im Folgenden erläutern werden.
Hier ist eine schematische Übersicht darüber, was wir gebaut haben und wie wir dieses dynamische Skalieren erreicht haben.
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
Der Kern dieser Einrichtung ist die systemd Socket-Aktivierung. Anstatt einen PostgreSQL-Container rund um die Uhr laufen zu lassen, hört das systemd-Init-System auf dem Datenbankport. Wenn eine Anwendung versucht, eine Verbindung herzustellen, fängt systemd die Anfrage ab, startet den Datenbankcontainer on-demand und übergibt dann die Verbindung. Sobald die Datenbank nicht mehr genutzt wird, wird sie automatisch heruntergefahren.
Dieser Ansatz kombiniert die Power standardisierter, bewährter Linux-Tools: systemd für das Service-Management und die Socket-Aktivierung sowie Docker Compose für die Definition unserer containerisierten Datenbankumgebung. Es ist einfach, robust und erfordert keine eigene Software.
Wir haben für diese Einrichtung zwei spezielle Technologieentscheidungen getroffen: PostgreSQL in einem Container laufen zu lassen und die Verwaltung mittels Docker Compose.
Entkopplung vom Host-Betriebssystem: Durch den Betrieb von PostgreSQL innerhalb eines Docker-Containers entkoppeln wir die Datenbankversion von der Version des Host-Systems. Das gibt uns die Flexibilität, unterschiedliche PostgreSQL-Versionen für verschiedene interne Dienste auf demselben Host parallel und konfliktfrei zu betreiben. Wir können eine Datenbank für einen Dienst upgraden, ohne die anderen zu beeinträchtigen.
Kompatibilität zu systemd: Wir haben Docker Compose gewählt, weil seine Lifecycle-Kommandos perfekt zu systemds Service-Management passen. Die ExecStart-Direktive von systemd erwartet einen Befehl, der im Vordergrund läuft, bis der Dienst gestoppt wird. docker-compose up erfüllt genau diese Anforderung. Eine klassischere Kombination aus docker create und docker start ist schwieriger zu managen, da systemd dann ein komplexeres Skript benötigen würde, um den Lifecycle zu steuern. docker-compose down bietet einen einzelnen, sauberen Befehl für die ExecStop-Direktive, der die gesamte Umgebung ordentlich herunterfährt.
Im Folgenden analysieren wir die Konfigurationsdateien, die das ermöglichen.
Wir verwenden eine Kombination aus einer docker-compose.yml-Datei zur Definition der Datenbank und drei systemd-Unit-Dateien, um den Scale-to-Zero-Lifecycle zu managen.
Dies ist eine standardmäßige docker-compose.yml-Datei. Sie definiert einen PostgreSQL-18-Container, mappt einen internen Port auf den Host und bindet ein Volume ein, um die Datenbankdaten auf der lokalen Festplatte zu persistieren. Dadurch bleiben die Daten auch erhalten, wenn der Container gestoppt wird. Alle in dem offiziellen PostgreSQL Image auf Docker Hub dokumentierten Einstellungen sind hier nutzbar und ermöglichen weitere Anpassungen wie das Anlegen spezifischer Nutzer oder Datenbanken beim Start.
/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
Diese .socket-Unit weist systemd an, auf Port 24532 auf allen Netzwerkschnittstellen zu lauschen. Wenn eine TCP-Verbindung eingeht, aktiviert systemd pg1-proxy.service. Das ist der Einstiegspunkt für alle Datenbankverbindungen.
/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
Hier steckt die On-Demand-Logik. Beim Aktivieren durch den Socket startet dieser Dienst zuerst den eigentlichen Datenbankdienst (Requires=pg1-postgres.service). Der ExecStartPre-Befehl ist eine kleine, aber entscheidende Shell-Schleife, die wiederholt prüft, ob der interne PostgreSQL-Port geöffnet ist. Ohne diese Überprüfung könnte es zu einer Rennbedingung kommen, bei der der Proxy die Verbindung des Clients vor dem vollständigen Hochfahren des PostgreSQL-Containers weiterleitet. Das würde für den Client sofort einen „Connection Refused“-Fehler bedeuten. Dieses Pre-Start-Skript sorgt dafür, dass die Übergabe reibungslos verläuft und der Client erst eine Verbindung erhält, wenn die Datenbank voll einsatzbereit ist.
Der Hauptprozess ist systemd-socket-proxyd, ein eingebautes Werkzeug, das die hereinkommende Verbindung an den internen Port weiterleitet, an dem der PostgreSQL-Container lauscht (127.0.0.1:14532). Der entscheidende Parameter ist --exit-idle-time=3min. Er weist den Proxy an, automatisch zu beenden, wenn er drei Minuten lang Leerlauf hatte.
/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
Dieser Dienst steuert den Lifecycle von Docker Compose. Er wird vom Proxy-Dienst gestartet. Die wichtige Direktive ist StopWhenUnneeded=true. Diese verknüpft seinen Lifecycle mit dem Proxy-Dienst. Wenn pg1-proxy.service beendet wird (weil der Idle-Timer abgelaufen ist), erkennt systemd, dass dieser Dienst nicht mehr benötigt wird, und stoppt ihn automatisch durch Ausführen von docker-compose down. Der Container wird heruntergefahren und alle Ressourcen werden freigegeben.
/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
Dieses Setup ist extrem effizient, bringt aber eine wichtige Überlegung mit sich: die „Cold Start“-Latenz. Die allererste Verbindung zur Datenbank nach einer Phase der Inaktivität verzögert sich. Der Client muss warten, bis systemd docker-compose up ausführt und der PostgreSQL-Container initialisiert ist. Nach unserer Erfahrung dauert das etwa eine Sekunde für eine kleine Datenbank, erhöht sich aber mit der Speichergröße.
Für viele interne Systeme – CI/CD, Batch-Jobs oder Admin-Dashboards mit sporadischer Nutzung – ist diese Verzögerung ein völlig akzeptabler Kompromiss angesichts der erheblichen Ressourceneinsparungen. Für hochfrequente, latenzkritische Produktionsanwendungen ist eine traditionelle, ständig verfügbare Datenbank weiterhin die richtige Wahl.
Um eine neue Datenbank online zu bringen, müssen wir nur die systemd-Units aktivieren.
1systemctl daemon-reload
2systemctl enable pg1-proxy.service
3systemctl enable pg1-postgres.service
4systemctl enable --now pg1-proxy.socket
Nach der Aktivierung ist die Datenbank bereit für Verbindungen, verbraucht aber keine Ressourcen, bis die erste Verbindung eingeht. Das ist ein weiterer kleiner Schritt auf unserem Weg, Verschwendung zu eliminieren, und zeigt, dass selbst essenzielle Infrastruktur wie eine relationale Datenbank schlank und bedarfsgesteuert betrieben werden kann.