Simplificar configuración de PostgreSQL con cadena de conexión única
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 16s

Eliminados hacks de autenticación md5 y configuración manual de pg_hba.conf.
Ahora usa NUXT_POSTGRES_URL como secret de Gitea para conexión directa.
This commit is contained in:
2025-11-22 00:31:47 -06:00
parent 2d04966388
commit 5b9445ca2d
8 changed files with 72 additions and 225 deletions

View File

@@ -27,6 +27,15 @@ APP_DOMAIN=miapp.ejemplo.com
# URL pública de la aplicación
NUXT_PUBLIC_APP_URL=https://miapp.ejemplo.com
# ===========================================
# DATABASE (PostgreSQL)
# ===========================================
POSTGRES_USER=seguidor
POSTGRES_PASSWORD=seguidor_password
POSTGRES_DB=seguidor_lotes
POSTGRES_PORT=5432
NUXT_POSTGRES_URL=postgres://seguidor:seguidor_password@postgres:5432/seguidor_lotes
# ===========================================
# REGISTRY AUTHENTICATION (solo para CI/CD)
# ===========================================

View File

@@ -17,6 +17,8 @@ jobs:
POSTGRES_USER: ${{ vars.POSTGRES_USER || 'seguidor' }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD || 'seguidor_password' }}
POSTGRES_DB: ${{ vars.POSTGRES_DB || 'seguidor_lotes' }}
POSTGRES_PORT: ${{ vars.POSTGRES_PORT || '5432' }}
NUXT_POSTGRES_URL: ${{ secrets.NUXT_POSTGRES_URL || vars.NUXT_POSTGRES_URL || '' }}
steps:
- uses: actions/checkout@v3
@@ -44,37 +46,17 @@ jobs:
docker compose pull
docker compose --project-name $APP_NAME down
# Levantar solo Postgres para sincronizar auth antes de iniciar la app
docker compose --project-name $APP_NAME up -d postgres
echo "⏳ Esperando a PostgreSQL..."
for i in $(seq 1 20); do
if docker exec $APP_NAME-postgres pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"; then
break
fi
sleep 1
done
# Asegurar autenticación md5 y que la contraseña coincide con la env (cura volúmenes viejos)
echo "🔐 Sincronizando autenticación PostgreSQL (md5 + password)..."
ESCAPED_PASSWORD=${POSTGRES_PASSWORD//\'/\'\"\'\"\'}
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" $APP_NAME-postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "ALTER SYSTEM SET password_encryption = 'md5';"
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" $APP_NAME-postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "ALTER ROLE \"$POSTGRES_USER\" WITH PASSWORD '${ESCAPED_PASSWORD}';"
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" $APP_NAME-postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "SELECT pg_reload_conf();"
# Ahora sí levantar la app
docker compose --project-name $APP_NAME up -d --remove-orphans --wait
# Inicializar base de datos si es necesario
echo "🗄️ Inicializando base de datos..."
# Verificar si las tablas existen
TABLE_EXISTS=$(docker exec $APP_NAME-postgres psql -U $POSTGRES_USER -d $POSTGRES_DB -tAc "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'lotes');")
TABLE_EXISTS=$(docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" $APP_NAME-postgres psql -U $POSTGRES_USER -d $POSTGRES_DB -tAc "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'lotes');")
if [ "$TABLE_EXISTS" = "f" ]; then
echo "📝 Ejecutando scripts de inicialización..."
docker exec -i $APP_NAME-postgres psql -U $POSTGRES_USER -d $POSTGRES_DB < nuxt4/server/database/01_schema.sql
docker exec -i $APP_NAME-postgres psql -U $POSTGRES_USER -d $POSTGRES_DB < nuxt4/server/database/02_seed.sql
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" -i $APP_NAME-postgres psql -U $POSTGRES_USER -d $POSTGRES_DB < nuxt4/server/database/01_schema.sql
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" -i $APP_NAME-postgres psql -U $POSTGRES_USER -d $POSTGRES_DB < nuxt4/server/database/02_seed.sql
echo "✅ Base de datos inicializada"
else
echo " Base de datos ya inicializada"

View File

@@ -89,8 +89,10 @@ UVA-001 (2086kg)
- `APP_NAME` - `lotes`
- `APP_DOMAIN` - `lotes.nucleoriofrio.com`
- `NUXT_PUBLIC_APP_URL` - `https://lotes.nucleoriofrio.com`
- `NUXT_POSTGRES_URL` - `postgres://seguidor:seguidor_password@postgres:5432/seguidor_lotes`
- `POSTGRES_USER` - `seguidor`
- `POSTGRES_DB` - `seguidor_lotes`
- `POSTGRES_PORT` - `5432`
3. **Push al repositorio** - El workflow automáticamente:
- Construye la imagen Docker
@@ -122,7 +124,6 @@ UVA-001 (2086kg)
│ │ │ ├── db.ts # Pool de PostgreSQL
│ │ │ └── queries.ts # Funciones SQL
│ │ └── database/
│ │ ├── 00_configure_auth.sh # Config autenticación
│ │ ├── 01_schema.sql # Esquema DB
│ │ └── 02_seed.sql # Datos de ejemplo
│ └── package.json
@@ -293,10 +294,7 @@ cd nuxt4
npm install
# Configurar PostgreSQL local
export POSTGRES_USER=seguidor
export POSTGRES_PASSWORD=seguidor_password
export POSTGRES_DB=seguidor_lotes
export POSTGRES_HOST=localhost
export NUXT_POSTGRES_URL=postgres://seguidor:seguidor_password@localhost:5432/seguidor_lotes
# Ejecutar scripts SQL
psql -U seguidor -d seguidor_lotes < server/database/01_schema.sql
@@ -313,9 +311,7 @@ cd nuxt4
npm install
# Apuntar a BD de producción
export POSTGRES_HOST=server.interno.com
export POSTGRES_PORT=5432
# ... resto de variables
export NUXT_POSTGRES_URL=postgres://seguidor:seguidor_password@server.interno.com:5432/seguidor_lotes
npm run dev
```

View File

@@ -9,6 +9,7 @@ services:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_PORT=${POSTGRES_PORT:-5432}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./nuxt4/server/database:/docker-entrypoint-initdb.d:ro
@@ -38,8 +39,7 @@ services:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
- NUXT_POSTGRES_URL=${NUXT_POSTGRES_URL:-postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:${POSTGRES_PORT:-5432}/${POSTGRES_DB}}
networks:
- principal
- traefik-network

View File

@@ -15,6 +15,7 @@ export default defineNuxtConfig({
css: ['~/assets/css/main.css'],
runtimeConfig: {
postgresUrl: process.env.NUXT_POSTGRES_URL || '',
public: {
authentikUrl: process.env.NUXT_PUBLIC_AUTHENTIK_URL || 'https://authentik.nucleoriofrio.com'
}
@@ -107,4 +108,4 @@ export default defineNuxtConfig({
type: 'module'
}
}
})
})

View File

@@ -1,32 +0,0 @@
#!/bin/bash
set -e
echo "Configurando autenticación de PostgreSQL..."
POSTGRES_USER="${POSTGRES_USER:-seguidor}"
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-seguidor_password}"
POSTGRES_DB="${POSTGRES_DB:-$POSTGRES_USER}"
# Eliminar configuración scram-sha-256 y agregar md5
# Esto asegura que las conexiones remotas funcionen correctamente
sed -i '/scram-sha-256/d' "$PGDATA/pg_hba.conf"
# Agregar configuración para conexiones md5 si no existe
if ! grep -q "host all all all md5" "$PGDATA/pg_hba.conf"; then
echo "host all all all md5" >> "$PGDATA/pg_hba.conf"
fi
# Forzar que las contraseñas se guarden en md5 (no scram) y
# reescribir la contraseña para asegurar que coincide con las env vars.
ESCAPED_PASSWORD=${POSTGRES_PASSWORD//\'/\'\'}
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
ALTER SYSTEM SET password_encryption = 'md5';
ALTER ROLE "$POSTGRES_USER" WITH PASSWORD '${ESCAPED_PASSWORD}';
SELECT pg_reload_conf();
EOSQL
echo "✓ Configuración de autenticación aplicada (md5 + password sincronizada)"
# Nota: No es necesario recargar aquí porque este script corre ANTES
# de que PostgreSQL termine su inicialización. Los cambios se aplican
# automáticamente cuando PostgreSQL termina de iniciarse.

View File

@@ -6,7 +6,7 @@ Este directorio contiene los scripts SQL para inicializar y gestionar la base de
- [Archivos SQL](#archivos-sql)
- [⚠️ Peculiaridades de Implementación](#-peculiaridades-de-implementación)
- [Autenticación PostgreSQL](#autenticación-postgresql)
- [Conexión de la app](#conexión-de-la-app)
- [Inicialización de la Base de Datos](#inicialización-de-la-base-de-datos)
- [Persistencia de Datos](#persistencia-de-datos)
- [Conexión desde Node.js](#conexión-desde-nodejs)
@@ -19,12 +19,6 @@ Este directorio contiene los scripts SQL para inicializar y gestionar la base de
## Archivos SQL
### `00_configure_auth.sh`
Script Bash que configura la autenticación de PostgreSQL:
- Elimina configuración `scram-sha-256` (incompatible con driver `pg` de Node.js)
- Configura método `md5` en `pg_hba.conf`
- Se ejecuta automáticamente durante inicialización de PostgreSQL
### `01_schema.sql`
Crea el esquema completo de la base de datos:
- Tablas: `lotes`, `operaciones`, `operacion_lotes`
@@ -52,51 +46,11 @@ Datos de ejemplo que representan un flujo completo de trazabilidad:
Esta sección documenta los desafíos técnicos encontrados y las soluciones implementadas durante el desarrollo.
### Autenticación PostgreSQL
### Conexión de la app
#### ❌ Problema: Incompatibilidad scram-sha-256
**Contexto**: PostgreSQL 16 usa por defecto el método de autenticación `scram-sha-256`, pero el driver `pg` de Node.js tiene problemas de compatibilidad que resultan en:
```
Error code: 28P01
FATAL: password authentication failed for user "seguidor"
```
#### ✅ Solución: Script de configuración automática
**Archivo**: `00_configure_auth.sh`
```bash
#!/bin/bash
set -e
echo "Configurando autenticación de PostgreSQL..."
# Eliminar configuración scram-sha-256
sed -i '/scram-sha-256/d' "$PGDATA/pg_hba.conf"
# Agregar configuración md5
if ! grep -q "host all all all md5" "$PGDATA/pg_hba.conf"; then
echo "host all all all md5" >> "$PGDATA/pg_hba.conf"
fi
# Forzar md5 para almacenamiento de passwords y reescribir contraseña
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
ALTER SYSTEM SET password_encryption = 'md5';
ALTER ROLE "$POSTGRES_USER" WITH PASSWORD '${POSTGRES_PASSWORD}';
SELECT pg_reload_conf();
EOSQL
echo "✓ Configuración de autenticación aplicada (md5)"
```
**Notas importantes**:
- El script se ejecuta **antes** de que PostgreSQL termine su inicialización
- Los cambios en `pg_hba.conf` se aplican automáticamente al finalizar el inicio
- No es necesario ejecutar `pg_ctl reload` manualmente
- El método `md5` es compatible con el driver `pg` de Node.js
- Cada deploy vuelve a aplicar `password_encryption=md5` y resetea la contraseña vía Gitea Action para curar volúmenes antiguos
- Se usa una **cadena de conexión única** (`NUXT_POSTGRES_URL`) consumida desde `runtimeConfig.postgresUrl`.
- Formato recomendado: `postgres://seguidor:seguidor_password@postgres:5432/seguidor_lotes`.
- El driver `pg` soporta `scram-sha-256` (default de Postgres 16), no se toca `pg_hba.conf`.
---
@@ -106,9 +60,8 @@ echo "✓ Configuración de autenticación aplicada (md5)"
Los scripts se ejecutan **solo si el volumen de datos está vacío** (primera vez):
1. **`00_configure_auth.sh`** - Configura autenticación md5
2. **`01_schema.sql`** - Crea estructura (tablas, funciones, vistas)
3. **`02_seed.sql`** - Inserta datos de ejemplo
1. **`01_schema.sql`** - Crea estructura (tablas, funciones, vistas)
2. **`02_seed.sql`** - Inserta datos de ejemplo
**Orden de ejecución**: Los archivos se ejecutan en orden alfabético/numérico.
@@ -216,42 +169,19 @@ git commit --allow-empty -m "Trigger reinit DB" && git push
**Configuración en `server/utils/db.ts`**:
```typescript
const config = {
user: process.env.POSTGRES_USER || 'seguidor',
password: process.env.POSTGRES_PASSWORD || 'seguidor_password',
database: process.env.POSTGRES_DB || 'seguidor_lotes',
host: process.env.POSTGRES_HOST || 'postgres',
port: parseInt(process.env.POSTGRES_PORT || '5432'),
max: 20, // Máximo de conexiones en el pool
idleTimeoutMillis: 30000, // 30s timeout para conexiones idle
connectionTimeoutMillis: 10000, // 10s timeout para establecer conexión
}
const config = useRuntimeConfig()
const pool = new Pool({
connectionString: config.postgresUrl, // Ej: postgres://user:pass@host:5432/db
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 10000,
})
```
**Peculiaridades**:
1. **Timeout de 10 segundos**: Necesario porque PostgreSQL puede tardar en aplicar configuración de autenticación
2. **Retry Logic Automático**:
```typescript
// Verifica conexión inicial con 5 reintentos
let retries = 5
const tryConnect = async () => {
try {
const client = await pool!.connect()
console.log('✅ Conexión inicial a PostgreSQL exitosa')
client.release()
} catch (err: any) {
retries--
if (retries > 0) {
console.log(`⚠️ Conexión falló, reintentando... (${retries} intentos)`)
setTimeout(tryConnect, 2000) // Reintentar en 2s
}
}
}
```
3. **Logs solo en desarrollo**:
1. **Cadena única**: evita discrepancias entre host/puerto/usuario/contraseña.
2. **Logs solo en desarrollo**:
```typescript
if (process.env.NODE_ENV !== 'production') {
pool.on('connect', () => {
@@ -262,20 +192,7 @@ const config = {
#### ⚡ Race Condition: App vs PostgreSQL
**Problema**: Aunque docker-compose tiene `depends_on` con `service_healthy`, el healthcheck solo verifica que PostgreSQL **responde**, no que terminó de ejecutar los scripts de inicialización.
**Timeline Real**:
```
T+0s: PostgreSQL inicia
T+2s: PostgreSQL responde a pg_isready → healthcheck ✅
T+2s: App Nuxt inicia e intenta conectarse
T+3s: PostgreSQL ejecuta 00_configure_auth.sh (cambia auth)
T+4s: Primera conexión de app FALLA (todavía usa scram-sha-256)
T+5s: Script termina, auth está configurado
T+7s: Retry logic de app conecta exitosamente ✅
```
**Solución**: El retry logic compensa esta race condition automáticamente.
Aunque docker-compose tiene `depends_on` con `service_healthy`, el healthcheck solo verifica que PostgreSQL responde. Si necesitas máxima robustez puedes añadir un pequeño retry en la app o un `pg_isready` antes de exponer el servicio, pero con la cadena de conexión única y el pool el arranque es estable.
---
@@ -380,12 +297,11 @@ curl -X POST https://lotes.nucleoriofrio.com/api/debug/seed-database
**Causas posibles**:
1. **Primera carga del sistema** (normal)
- PostgreSQL todavía está configurando autenticación
- Solución: Esperar ~5 segundos, el retry logic se encarga
- Logs: `⚠️ Conexión falló, reintentando...` seguido de `✅ Conexión exitosa`
- PostgreSQL todavía está terminando de iniciar
- Solución: Esperar unos segundos y reintentar
2. **Volumen de postgres de un deploy anterior con scram-sha-256**
- Solución: Eliminar volumen y redeploy
2. **Volumen con contraseña diferente a la actual**
- Solución: Eliminar volumen y redeploy para reinicializar
```bash
docker-compose --project-name lotes down -v
git commit --allow-empty -m "Reinit DB" && git push
@@ -393,7 +309,7 @@ curl -X POST https://lotes.nucleoriofrio.com/api/debug/seed-database
3. **Variables de entorno incorrectas**
- Verificar en Gitea > Settings > Actions > Secrets/Variables
- Variables requeridas: `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`
- Variables requeridas: `NUXT_POSTGRES_URL`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`
### ❌ Error: "relation 'lotes' does not exist"
@@ -529,7 +445,6 @@ ORDER BY total DESC;
4. Ejecuta /docker-entrypoint-initdb.d/ en orden:
├─ 00_configure_auth.sh → Cambia pg_hba.conf a md5
├─ 01_schema.sql → Crea tablas, funciones, vistas
└─ 02_seed.sql → Inserta 10 lotes, 7 ops, 16 rels
@@ -600,9 +515,9 @@ docker exec -i lotes-postgres psql -U seguidor -d seguidor_lotes < backup.sql
| Decisión | Razón | Alternativa Descartada |
|----------|-------|------------------------|
| md5 en lugar de scram-sha-256 | Compatibilidad con driver pg de Node.js | Actualizar driver (requiere cambios mayores) |
| Retry logic en pool | Compensa race condition de inicialización | depends_on custom healthcheck (complejo) |
| Timeout de 10s | PostgreSQL puede tardar en configurar auth | 2s (insuficiente) |
| Cadena de conexión única (`NUXT_POSTGRES_URL`) | Evita discrepancias entre host/puerto/usuario/contraseña | Variables sueltas (`POSTGRES_*`) en el runtime de la app |
| No tocar `pg_hba.conf` (usa scram por defecto) | Menos acoplamiento a la imagen y a volúmenes existentes | Forzar md5 en init scripts |
| Timeout de 10s en el pool | PostgreSQL puede tardar unos segundos en aceptar conexiones | Timeout bajo (más falsos negativos) |
| DROP TABLE en reset | Workflow detecta ausencia y reinicializa | TRUNCATE (deja tablas vacías) |
| seed ejecuta schema primero | Funciona después de borrar BD | Solo seed (falla si no hay tablas) |
| Scripts SQL en app container | Permite endpoint de seed | Solo en postgres (no accesible desde app) |

View File

@@ -1,60 +1,49 @@
import pg from 'pg'
import { useRuntimeConfig } from '#imports'
const { Pool } = pg
let pool: pg.Pool | null = null
function buildConnectionString(): string {
const config = useRuntimeConfig()
if (config.postgresUrl) {
return config.postgresUrl
}
// Fallback para entornos locales si no se pasó NUXT_POSTGRES_URL
const user = process.env.POSTGRES_USER || 'seguidor'
const password = process.env.POSTGRES_PASSWORD || 'seguidor_password'
const host = process.env.POSTGRES_HOST || 'postgres'
const port = process.env.POSTGRES_PORT || '5432'
const db = process.env.POSTGRES_DB || 'seguidor_lotes'
return `postgres://${user}:${password}@${host}:${port}/${db}`
}
/**
* Obtiene o crea el pool de conexiones a PostgreSQL.
* Usa variables de entorno para la configuración.
* Usa cadena de conexión única para mantener consistencia.
*/
export function getPool(): pg.Pool {
if (!pool) {
const config = {
user: process.env.POSTGRES_USER || 'seguidor',
password: process.env.POSTGRES_PASSWORD || 'seguidor_password',
database: process.env.POSTGRES_DB || 'seguidor_lotes',
host: process.env.POSTGRES_HOST || 'postgres',
port: parseInt(process.env.POSTGRES_PORT || '5432'),
max: 20, // máximo de conexiones en el pool
const connectionString = buildConnectionString()
pool = new Pool({
connectionString,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 10000, // Aumentado a 10s para la primera conexión
}
connectionTimeoutMillis: 10000,
})
pool = new Pool(config)
// Manejo de errores del pool
pool.on('error', (err) => {
console.error('Error inesperado en el pool de PostgreSQL:', err)
})
// Solo log en desarrollo para reducir ruido
if (process.env.NODE_ENV !== 'production') {
pool.on('connect', () => {
console.log('Nueva conexión establecida con PostgreSQL')
})
}
// Verificar conexión inicial con retry
let retries = 5
const tryConnect = async () => {
try {
const client = await pool!.connect()
console.log('✅ Conexión inicial a PostgreSQL exitosa')
client.release()
} catch (err: any) {
retries--
if (retries > 0) {
console.log(`⚠️ Conexión a PostgreSQL falló, reintentando... (${retries} intentos restantes)`)
setTimeout(tryConnect, 2000) // Reintentar en 2 segundos
} else {
console.error('❌ No se pudo conectar a PostgreSQL después de varios intentos:', err.message)
}
}
}
// Ejecutar verificación inicial en background
tryConnect()
}
return pool
@@ -62,11 +51,6 @@ export function getPool(): pg.Pool {
/**
* Ejecuta una query SQL con parámetros.
* Wrapper seguro para evitar inyección SQL.
*
* @param text - Query SQL con placeholders $1, $2, etc.
* @param params - Parámetros para la query
* @returns Resultado de la query
*/
export async function query<T = any>(
text: string,
@@ -79,7 +63,6 @@ export async function query<T = any>(
const result = await pool.query<T>(text, params)
const duration = Date.now() - start
// Log solo en desarrollo
if (process.env.NODE_ENV !== 'production') {
console.log('Query ejecutada:', { text, duration: `${duration}ms`, rows: result.rowCount })
}
@@ -93,9 +76,6 @@ export async function query<T = any>(
/**
* Obtiene un cliente del pool para ejecutar transacciones.
* IMPORTANTE: Debes llamar a client.release() al terminar.
*
* @returns Cliente de PostgreSQL
*/
export async function getClient(): Promise<pg.PoolClient> {
const pool = getPool()
@@ -103,8 +83,7 @@ export async function getClient(): Promise<pg.PoolClient> {
}
/**
* Cierra el pool de conexiones.
* Útil para tests o shutdown graceful.
* Cierra el pool de conexiones (tests o shutdown).
*/
export async function closePool(): Promise<void> {
if (pool) {
@@ -116,9 +95,6 @@ export async function closePool(): Promise<void> {
/**
* Verifica que la conexión a la base de datos esté funcionando.
* Útil para health checks.
*
* @returns true si la conexión está OK, false en caso contrario
*/
export async function checkConnection(): Promise<boolean> {
try {