Siempre buscamos construir servicios lo más eficientes posible en cuanto a recursos. Esto es cierto para los servicios que ofrecemos externamente y, igualmente importante, para nuestra propia infraestructura interna. Muchas de nuestras herramientas internas, como los sistemas de facturación y monitoreo, dependen de bases de datos PostgreSQL. Aunque son esenciales, estas bases de datos a menudo permanecen inactivas por largos periodos, consumiendo RAM y ciclos de CPU sin motivo.
Así que nos preguntamos: ¿podemos aplicar nuestra filosofía scale-to-zero a nuestras propias bases de datos? La respuesta es sí. Hemos desarrollado un sistema para aprovisionar instancias de PostgreSQL que sólo se ejecutan cuando están activamente en uso. Este diseño es increíblemente eficiente en recursos, pero sí conlleva algunos compromisos, que exploraremos.
Aquí hay un esquema general de lo que construimos y cómo logramos esta escalabilidad dinámica.
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
El núcleo de esta configuración es la activación por socket de systemd. En lugar de tener un contenedor PostgreSQL corriendo 24/7, dejamos que el sistema init systemd escuche en el puerto de la base de datos. Cuando una aplicación intenta conectarse, systemd intercepta la solicitud, inicia el contenedor de la base de datos bajo demanda y luego entrega la conexión. Una vez que la base de datos ya no está en uso, se apaga automáticamente.
Este enfoque combina el poder de herramientas estándar y probadas en Linux: systemd para la gestión de servicios y activación por socket, y Docker Compose para definir nuestro entorno de base de datos contenedorizado. Es simple, robusto y no requiere software personalizado.
Hicimos dos elecciones tecnológicas específicas para esta configuración: ejecutar PostgreSQL en un contenedor y gestionarlo con Docker Compose.
Desacoplamiento del SO anfitrión: Al ejecutar PostgreSQL dentro de un contenedor Docker, desacoplamos la versión de la base de datos de la versión del sistema operativo anfitrión. Esto nos da la flexibilidad para ejecutar diferentes versiones de PostgreSQL para distintos servicios internos en el mismo host sin conflictos ni problemas de dependencia. Podemos actualizar una base de datos para un servicio sin impactar a los demás.
Compatibilidad con systemd: Elegimos Docker Compose porque sus comandos de ciclo de vida encajan perfectamente con cómo systemd maneja servicios. La directiva ExecStart de systemd espera un comando que se ejecute en primer plano hasta que el servicio se detenga. docker-compose up hace exactamente eso. Una semántica más clásica de docker create seguida de docker start es más difícil de manejar, porque systemd necesitaría un script más complejo para manejar el ciclo de vida. docker-compose down provee un comando único y limpio para la directiva ExecStop, asegurando que todo el entorno se cierre de forma ordenada.
Desglosemos los archivos de configuración que hacen esto posible.
Usamos una combinación de un archivo docker-compose.yml para definir la base de datos y tres unidades de systemd para manejar el ciclo de vida scale-to-zero.
Este es un archivo docker-compose.yml estándar. Define un contenedor PostgreSQL 18, mapea un puerto interno al host y monta un volumen para persistir los datos de la base de datos en el disco local. Esto asegura que aunque el contenedor se detenga, los datos permanezcan seguros. Todos los ajustes documentados en la imagen oficial de PostgreSQL en Docker Hub se pueden usar aquí, permitiendo personalizaciones adicionales, como crear usuarios o bases de datos específicas al iniciar.
/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
Esta unidad .socket le indica a systemd que escuche en el puerto 24532 en todas las interfaces de red. Cuando llega una conexión TCP, systemd activará pg1-proxy.service. Este es el punto de entrada para todas las conexiones a la base de datos.
/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
Aquí vive la lógica bajo demanda. Cuando se activa por el socket, este servicio primero inicia el servicio real de la base de datos (Requires=pg1-postgres.service). El comando ExecStartPre es un pequeño pero crítico bucle de shell que revisa repetidamente si el puerto interno de PostgreSQL está abierto. Sin esta revisión, podría ocurrir una condición de carrera donde el proxy comienza y reenvía la conexión del cliente antes de que el contenedor PostgreSQL termine de inicializarse. Esto resultaría en un error inmediato de “Conexión Rechazada” para el cliente. Este script previo al inicio asegura que la entrega sea suave y que el cliente sólo se conecte cuando la base de datos esté completamente lista.
El proceso principal es systemd-socket-proxyd, una herramienta integrada que reenvía la conexión entrante al puerto interno donde escucha el contenedor PostgreSQL (127.0.0.1:14532). La parte crucial es --exit-idle-time=3min. Esto indica al proxy que salga automáticamente si ha estado inactivo por tres minutos.
/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
Este servicio maneja el ciclo de vida de Docker Compose. Es iniciado por el servicio proxy. La directiva clave es StopWhenUnneeded=true. Esto vincula su ciclo de vida al servicio proxy. Cuando pg1-proxy.service se detiene (porque expiró su temporizador de inactividad), systemd ve que este servicio ya no es necesario y lo detiene automáticamente ejecutando docker-compose down. El contenedor se apaga, liberando todos sus recursos.
/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
Esta configuración es increíblemente eficiente, pero tiene una consideración importante: la latencia de “arranque en frío”. La primera conexión a la base de datos tras un período de inactividad se retrasará. El cliente debe esperar a que systemd ejecute docker-compose up y a que el contenedor PostgreSQL se inicialice. En nuestra experiencia, esto toma cerca de un segundo para una base de datos pequeña, pero el tiempo aumenta con el tamaño del almacenamiento.
Para muchos sistemas internos — CI/CD, trabajos batch o paneles administrativos de uso poco frecuente — esta demora es un compromiso aceptable a cambio del significativo ahorro de recursos. Para aplicaciones productivas con alto tráfico y sensibilidad a la latencia, una base de datos tradicional, siempre encendida, sigue siendo la opción correcta.
Para poner en línea una nueva base de datos, sólo necesitamos habilitar las unidades systemd.
1systemctl daemon-reload
2systemctl enable pg1-proxy.service
3systemctl enable pg1-postgres.service
4systemctl enable --now pg1-proxy.socket
Una vez habilitadas, la base de datos está lista para aceptar conexiones, pero no consumirá recursos hasta que llegue la primera. Este es otro pequeño paso en nuestra misión para eliminar el desperdicio, demostrando que incluso una infraestructura esencial como una base de datos relacional puede ser ejecutada de forma ágil y bajo demanda.