Siempre buscamos construir servicios lo más eficientes posible en cuanto a recursos. Esto es cierto para los servicios que ofrecemos externamente y, igual de importante, para nuestra propia infraestructura interna. Muchas de nuestras herramientas internas, como sistemas de facturación y monitoreo, dependen de bases de datos PostgreSQL. Aunque son esenciales, estas bases de datos a menudo permanecen inactivas durante largos periodos, consumiendo RAM y ciclos de CPU sin razón.
Entonces, nos preguntamos: ¿podemos aplicar nuestra filosofía de scale-to-zero a nuestras propias bases de datos? La respuesta es sí. Hemos desarrollado un sistema para aprovisionar instancias PostgreSQL que solo se ejecutan cuando están siendo usadas activamente. Este diseño es increíblemente eficiente en cuanto a recursos, pero sí implica algunos compromisos, que exploraremos.
Aquí hay una vista esquemática 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 ejecutándose 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 transfiere 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 de Linux: systemd para la gestión de servicios y activación por socket, y Docker Compose para definir nuestro entorno de base de datos en contenedores. Es simple, robusto y no requiere software personalizado.
Tomamos dos decisiones 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 dependencias. Podemos actualizar una base de datos para un servicio sin afectar a los demás.
Compatibilidad con systemd: Elegimos Docker Compose porque sus comandos de ciclo de vida encajan perfectamente con la forma en que systemd maneja los servicios. La directiva ExecStart de systemd espera un comando que se ejecute en primer plano hasta que el servicio sea detenido. docker-compose up hace exactamente esto. Una semántica más clásica de docker create seguido por docker start es más difícil de manejar, ya que systemd necesitaría un script más complejo para manejar el ciclo de vida. docker-compose down proporciona un comando único y limpio para la directiva ExecStop, asegurando que todo el entorno se derrumbe ordenadamente.
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 systemd para gestionar el ciclo de vida scale-to-zero.
Este es un archivo estándar docker-compose.yml. 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. Todas las configuraciones documentadas en la imagen oficial de PostgreSQL en Docker Hub pueden usarse aquí, permitiendo una mayor personalización como crear usuarios o bases de datos específicos 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 de 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 es activado por el socket, este servicio primero inicia el servicio real de base de datos (Requires=pg1-postgres.service). El comando ExecStartPre es un pequeño pero crítico bucle shell que revisa repetidamente si el puerto interno de PostgreSQL está abierto. Sin esta verificación, podría ocurrir una condición de carrera donde el proxy arranca 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 asegura que la transferencia sea fluida y que el cliente sólo se conecte una vez que 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 el contenedor PostgreSQL está escuchando (127.0.0.1:14532). La parte clave es --exit-idle-time=3min. Esto indica al proxy que salga automáticamente si ha estado inactivo durante 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 gestiona 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 su temporizador de inactividad expiró), 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 trae una consideración importante: la latencia del “arranque en frío”. La primera conexión a la base de datos luego de un periodo de inactividad tendrá un retraso. El cliente debe esperar a que systemd ejecute docker-compose up y que el contenedor PostgreSQL se inicialice. En nuestra experiencia, esto lleva alrededor de un segundo para una base de datos pequeña, pero aumenta con el tamaño del almacenamiento.
Para muchos sistemas internos—CI/CD, trabajos batch o paneles administrativos de uso poco frecuente—este retraso es un compromiso perfectamente aceptable por el ahorro significativo de recursos. Para aplicaciones de alta carga y sensibles a la latencia en producción, una base de datos tradicional siempre activa sigue siendo la opción correcta.
Para poner en línea una nueva base de datos, solo 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 habilitada, 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 de eliminar el desperdicio, demostrando que incluso infraestructura esencial como una base de datos relacional puede ejecutarse de forma ágil y bajo demanda.