Scale-To-Zero postgresql databases

created: Mittwoch, Okt. 1, 2025

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

Die Magie der Socket-Aktivierung

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.

Unsere Technologieentscheidungen: Warum Container und Docker Compose?

Wir haben für diese Einrichtung zwei spezielle Technologieentscheidungen getroffen: PostgreSQL in einem Container laufen zu lassen und die Verwaltung mittels Docker Compose.

Im Folgenden analysieren wir die Konfigurationsdateien, die das ermöglichen.

Die Komponenten

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.

1. Die Datenbankdefinition: Docker Compose

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

2. Der Listener: systemd Socket

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

3. Der Proxy und Idle-Timer: systemd Service

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

4. Der Container-Manager: systemd Service

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

Der Kompromiss: Cold Starts

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.

Den Dienst aktivieren

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.