PostgreSQL Self-Hosted con Docker y pgvector: Base de Datos para tus Servicios
- Fabrizio Piminchumo
- Application , Technology
- 15 Apr, 2026
En la arquitectura de alta disponibilidad con n8n que vimos anteriormente, uno de los requisitos era reemplazar SQLite por una base de datos real. PostgreSQL es la elección natural para un entorno self-hosted: es robusto, soporta concurrencia, y es compatible con prácticamente todos los servicios que iremos desplegando en esta serie.
Pero más allá de PostgreSQL estándar, en ECODING usamos la imagen pgvector — una variante que incluye soporte nativo para vectores. Esto nos permite usar la misma instancia para aplicaciones de IA sin levantar un servicio adicional.
¿Por qué pgvector y no postgres estándar?
La imagen oficial pgvector/pgvector:pg16 es PostgreSQL 16 con la extensión vector preinstalada. Esta extensión permite almacenar y consultar embeddings (representaciones numéricas de texto, imágenes o cualquier dato) directamente en la base de datos.
Si en algún momento conectas herramientas como n8n con nodos de IA, o despliegas aplicaciones RAG (Retrieval-Augmented Generation), ya tienes la infraestructura lista. No necesitas migrar a otra base de datos ni levantar una instancia separada como Qdrant o Pinecone para casos simples.
Para activar la extensión en cualquier base de datos:
CREATE EXTENSION IF NOT EXISTS vector;
Si nunca usas IA, pgvector se comporta exactamente igual que PostgreSQL estándar. No hay costo por tenerla instalada.
El docker-compose.yml
services:
postgres:
image: pgvector/pgvector:pg16
container_name: postgres
restart: unless-stopped
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: TU_PASSWORD_SEGURO
POSTGRES_DB: appdb
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "127.0.0.1:5432:5432"
networks:
- backend-net
volumes:
postgres_data:
networks:
backend-net:
external: true
Análisis de las decisiones de configuración
La imagen: pgvector/pgvector:pg16
image: pgvector/pgvector:pg16
El tag pg16 indica PostgreSQL 16 con pgvector. Si prefieres la imagen oficial sin pgvector, usa postgres:16. Ambas son intercambiables para uso general — la diferencia es solo la extensión adicional.
¿Cuándo usar cada una?
| Imagen | Cuándo usarla |
|---|---|
pgvector/pgvector:pg16 | Cuando tienes o planeas tener flujos de IA, embeddings, o búsqueda semántica |
postgres:16 | Entorno puramente relacional, sin IA |
container_name y hostname en la red
container_name: postgres
El container_name cumple dos funciones: define el nombre del contenedor en Docker (docker ps) y registra ese nombre como hostname adicional dentro de la red Docker. Esto significa que otros servicios pueden referenciarlo por ese nombre.
En el stack de n8n que vimos anteriormente, la variable de conexión es:
- DB_POSTGRESDB_HOST=postgres
Ese valor coincide con el container_name definido aquí. Sin container_name, el hostname en la red sería simplemente el nombre del servicio en el compose (postgres en este caso), pero con servicios que tienen nombres ambiguos o compuestos, fijarlo explícitamente evita confusiones.
restart: unless-stopped vs always
restart: unless-stopped
Esta diferencia es sutil pero importante:
| Política | Comportamiento |
|---|---|
always | Se reinicia siempre, incluso si fue detenido manualmente |
unless-stopped | Se reinicia tras fallas o reinicios del servidor, pero respeta un docker stop manual |
Para una base de datos, unless-stopped es la elección correcta. Si necesitas detener PostgreSQL para mantenimiento (backup, upgrade de imagen), no quieres que Docker lo vuelva a levantar automáticamente mientras trabajas. Con always, tendría que hacer docker stop y luego contrarrestar el reinicio automático.
Para n8n usamos always porque si el proceso muere queremos recuperación total sin intervención. Para infraestructura que tocamos manualmente con más frecuencia, unless-stopped da más control.
Puerto: enlazar solo a localhost
La configuración original expone el puerto directamente:
ports:
- "5432:5432" # ⚠️ accesible desde cualquier IP del servidor
Esto significa que cualquier IP que llegue al servidor puede intentar conectarse a PostgreSQL en el puerto 5432. Para producción, la recomendación es enlazarlo solo a la interfaz local:
ports:
- "127.0.0.1:5432:5432" # solo accesible desde el propio servidor
Así, la base de datos solo es alcanzable desde el host local o desde dentro de la red Docker (backend-net). Si necesitas acceso remoto para administración, hazlo a través de un túnel SSH:
ssh -L 5432:localhost:5432 usuario@tu-servidor
Esto te permite conectar con cualquier cliente PostgreSQL (pgAdmin, DBeaver, psql) desde tu máquina local de forma segura.
Volumen local vs externo
volumes:
postgres_data: # volumen local gestionado por Docker
A diferencia del volumen n8n_data que definimos como external: true, aquí el volumen es gestionado por Docker Compose directamente. La diferencia práctica:
- Volumen externo (
external: true): debes crearlo manualmente antes dedocker compose up. Si no existe, el comando falla. Útil cuando el volumen es compartido entre múltiples stacks. - Volumen local (sin
external): Docker lo crea automáticamente si no existe al levantar el stack. Más simple para stacks independientes.
Para PostgreSQL, el volumen no es compartido con ningún otro servicio, así que no necesita ser externo. Si haces docker compose down, el volumen persiste — solo se elimina con docker compose down -v, que debes ejecutar conscientemente.
Usuario root como superusuario inicial
POSTGRES_USER: root
POSTGRES_PASSWORD: TU_PASSWORD_SEGURO
POSTGRES_DB: appdb
El POSTGRES_USER en la imagen de PostgreSQL crea el superusuario inicial con ese nombre. Usar root funciona, pero para producción es recomendable crear usuarios dedicados por aplicación con permisos mínimos. Esto limita el impacto si las credenciales de una aplicación se ven comprometidas:
-- Crear usuario dedicado para n8n
CREATE USER n8n_user WITH PASSWORD 'otra_password_segura';
CREATE DATABASE n8n_db OWNER n8n_user;
GRANT ALL PRIVILEGES ON DATABASE n8n_db TO n8n_user;
Así cada servicio tiene su propia base de datos y credenciales, sin acceso a las demás.
Bases de datos por servicio
La instancia de PostgreSQL actúa como servidor compartido para todos tus servicios self-hosted. La variable POSTGRES_DB define solo la base de datos inicial — puedes crear todas las que necesites después.
Estructura recomendada para la serie:
| Servicio | Base de datos | Usuario |
|---|---|---|
| n8n | n8n_db | n8n_user |
| Aplicación propia | appdb | app_user |
| Otros servicios | servicio_db | servicio_user |
Levantar el stack
# La red debe existir (se crea una sola vez por servidor)
docker network create backend-net
# Levantar PostgreSQL
docker compose up -d
# Verificar que está corriendo
docker compose ps
# Conectarse a psql dentro del contenedor
docker exec -it postgres psql -U root -d appdb
# Ver logs
docker compose logs -f postgres
Conectar desde n8n
Si seguiste el post anterior de n8n, las variables de entorno ya apuntan a este servicio:
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n_db
- DB_POSTGRESDB_USER=n8n_user
- DB_POSTGRESDB_PASSWORD=TU_PASSWORD_SEGURO
Ambos stacks deben estar en la misma red (backend-net) para que la resolución de hostname funcione. No es necesario exponer el puerto 5432 al host para que n8n se conecte — la comunicación ocurre internamente en la red Docker.
Consideraciones para producción
- Backups automáticos: Los datos de PostgreSQL viven en el volumen
postgres_data. Configura backups periódicos conpg_dump:
docker exec postgres pg_dump -U root n8n_db > backup_$(date +%Y%m%d).sql
-
Logs de conexiones lentas: Agrega
command: postgres -c log_min_duration_statement=1000para registrar queries que tarden más de 1 segundo. -
Límite de conexiones: PostgreSQL 16 tiene un máximo de 100 conexiones concurrentes por defecto. Si tienes múltiples servicios conectados, considera aumentarlo en
postgresql.confo usar PgBouncer como pooler de conexiones.
En el próximo post de la serie veremos la configuración de Redis, que completa la infraestructura base para n8n en modo queue y para cualquier otro servicio que necesite caché o colas.