Por qué abandonar una base de datos gestionada
Las bases de datos gestionadas resuelven un problema real: eliminan las operaciones del primer día (instalación, replicación, copias de seguridad automáticas). Pero su modelo de precios está diseñado para que la factura escale de forma no lineal con el crecimiento. En AWS RDS, cada componente se factura por separado: el cómputo de la instancia, el almacenamiento gp3 por GB, los IOPS provisionados, el nodo secundario Multi-AZ, las copias de seguridad más allá de la ventana incluida, las transferencias de datos salientes. Una instancia db.r6g.xlarge en Multi-AZ con 200 GB de almacenamiento gp3 puede superar fácilmente los 800 $/mes en us-east-1; con 500 GB, se cruzan los 3.000 $/mes. La misma carga de trabajo en un VPS dedicado funciona a una fracción de ese coste.
El coste no es la única variable. Las restricciones de extensiones son un obstáculo técnico concreto: pgvector en RDS está disponible desde 2023, pero pg_graphql o ciertas versiones de d_btree_gist no lo están. En Supabase, el catálogo es más amplio pero está vinculado a la plataforma. El cumplimiento de datos es un tercer motor: almacenar una base de datos en el centro de datos de su elección, sin transferencias entre regiones facturables, cambia la ecuación para arquitecturas sujetas al RGPD o políticas de residencia de datos.
Qué conserva, qué gestionará usted mismo
- Conserva: PostgreSQL en la versión de su elección, todos sus datos, sus extensiones, sus roles y privilegios, sus cadenas de conexión una vez actualizada la variable
DATABASE_URL. - Conserva: el rendimiento — en un VPS NVMe bien dimensionado, las lecturas y escrituras aleatorias de PostgreSQL son al menos equivalentes a las de una instancia gestionada de la misma categoría.
- Gestiona: las copias de seguridad —
pg_dumpen cron, opg_basebackuppara una copia física; los archivos deben almacenarse fuera del servidor (almacenamiento de objetos, segundo VPS). - Gestiona: las actualizaciones menores (parches de seguridad) y las actualizaciones mayores (migración de versión) — la frecuencia es baja, el procedimiento está documentado, pero nadie las aplica en su lugar.
- Gestiona: las conexiones SSL — generar un certificado, configurar
ssl = onenpostgresql.conf, imponer TLS enpg_hba.conf; sin esto los datos transitan en texto claro. - Gestiona: la monitorización —
pg_stat_statements,pg_stat_activity, alertas sobre espacio en disco y conexiones activas; los paneles del proveedor desaparecen con la base de datos gestionada.
Antes de migrar — lista de prerrequisitos
Antes de ejecutar el primer pg_dump, cuatro puntos determinan el éxito de la migración.
Versión de PostgreSQL: fuente vs destino. Un volcado producido por pg_dump de la versión N puede restaurarse en la versión N o superior, nunca inferior. Compruebe la versión fuente: psql -h <endpoint> -U <user> -c 'SELECT version();'. En el VPS, instale la misma versión principal o superior desde el repositorio PGDG.
Extensiones activas. Liste las extensiones instaladas en la fuente: SELECT extname, extversion FROM pg_extension ORDER BY extname;. Algunas están disponibles universalmente (uuid-ossp, hstore, pgcrypto). Otras requieren un paquete de sistema adicional o compilación desde el código fuente. Las extensiones propias de Supabase (pg_graphql, pg_net, pgsodium) no existen en un PostgreSQL estándar: planifique su sustitución antes de migrar.
Tamaño de la base de datos. SELECT pg_size_pretty(pg_database_size('<base>')); da el tamaño lógico. En el VPS, prevea espacio en disco para el volcado, la base restaurada y un 20% de margen para WAL e índices temporales de restauración.
Conexiones activas. Una base de datos con tráfico no puede volcarse limpiamente con transacciones largas en curso. Compruebe: SELECT count(*), state FROM pg_stat_activity GROUP BY state;. Si persisten conexiones en estado idle in transaction durante varios minutos, identifique y termine esas sesiones antes de lanzar el volcado de migración.
Migrar desde AWS RDS
RDS impone TLS en las conexiones entrantes (rds.force_ssl = 1 por defecto en instancias recientes). El flag --sslmode=require es por tanto indispensable en pg_dump, de lo contrario la conexión se rechaza o se establece sin cifrado.
Use un endpoint de lectura (Read Replica) para el volcado siempre que sea posible: evita añadir carga a la instancia primaria durante la migración.
export PGPASSWORD='su-contraseña'
pg_dump \
-h su-instancia.xxxx.rds.amazonaws.com \
-U su_usuario \
--sslmode=require \
-Fc \
-f /tmp/dump-rds.pgc \
su_baseLuego restaure en el VPS en paralelo:
pg_restore \
--no-owner \
--no-privileges \
-h localhost \
-U su_usuario_vps \
-d su_base \
-j 4 \
/tmp/dump-rds.pgc--no-owner y --no-privileges son indispensables: RDS crea roles internos (rdsadmin, rds_superuser) ausentes de cualquier PostgreSQL fuera de AWS. Variables RDS no transferibles: rds.force_ssl y cualquier parámetro con prefijo rds. son parámetros de sistema propietarios de AWS — no existen en postgresql.conf estándar.
Migrar desde Google Cloud SQL
Cloud SQL ofrece dos vías de exportación: el Cloud SQL Auth Proxy (recomendado para conexiones programáticas) y la exportación a un bucket de GCS (recomendado para volcados grandes o migraciones unidireccionales).
Vía Cloud SQL Auth Proxy (conexión directa, adecuado para bases < 10 GB):
cloud_sql_proxy -instances=mi-proyecto:europe-west1:mi-instancia=tcp:5433 &
export PGPASSWORD='su-contraseña'
pg_dump \
-h 127.0.0.1 \
-p 5433 \
-U su_usuario \
-Fc \
-f /tmp/dump-cloudsql.pgc \
su_baseVía exportación GCS (recomendado para bases grandes):
gcloud sql export sql mi-instancia \
gs://mi-bucket/dump-cloudsql.sql \
--database=su_base \
--offload
gsutil cp gs://mi-bucket/dump-cloudsql.sql /tmp/
psql -h localhost -U su_usuario_vps -d su_base \
-f /tmp/dump-cloudsql.sqlEl flag --offload desplaza el procesamiento del volcado a un trabajador secundario y reduce el impacto en la instancia primaria. Cloud SQL también crea un usuario cloudsqladmin — aplique --no-owner --no-privileges a la restauración como con RDS.
Migrar desde Supabase
Supabase expone acceso directo a PostgreSQL a través de una URI de conexión disponible en los ajustes del proyecto (Settings > Database > Connection string > URI).
export DATABASE_URL="postgresql://postgres:su-contraseñ[email protected]:5432/postgres"
pg_dump \
"$DATABASE_URL" \
--sslmode=require \
-Fc \
--no-owner \
--no-privileges \
-f /tmp/dump-supabase.pgcExtensiones presentes en Supabase ausentes de un VPS estándar. Esta es la principal trampa al migrar desde Supabase:
- pgvector: disponible en Supabase, debe instalarse manualmente en el VPS (sudo apt install postgresql-17-pgvector en Debian/Ubuntu, luego CREATE EXTENSION vector;).
- pg_graphql: extensión propietaria de Supabase que expone un endpoint GraphQL automático. No existe en los repositorios APT estándar — compilar desde el código fuente (Rust + cargo-pgrx necesario) o reescribir la capa de acceso GraphQL fuera de la base de datos.
- pg_net: permite peticiones HTTP desde funciones SQL. Ausente de los paquetes estándar — reescriba los usos fuera de la base de datos.
- pgsodium: cifrado a nivel de base de datos. Su integración con Vault es específica de Supabase.
- http: extensión de la comunidad disponible como postgresql-17-http en Debian/Ubuntu.
Antes de lanzar el volcado, liste exhaustivamente las extensiones activas: SELECT extname FROM pg_extension WHERE extname NOT IN ('plpgsql');
Comandos por fuente
Desplace la tabla
| Fuente | Comando pg_dump | Trampas conocidas | Variable a reconfigurar |
|---|---|---|---|
| AWS RDS | `pg_dump -h <endpoint> -U <user> --sslmode=require -Fc -f dump.pgc <base>` | `--no-owner --no-privileges` requerido; roles `rdsadmin`/`rds_superuser` ausentes del VPS | `rds.force_ssl` → reemplazar por `ssl = on` en `postgresql.conf` |
| Google Cloud SQL | `pg_dump -h 127.0.0.1 -p 5433 -U <user> -Fc -f dump.pgc <base>` vía Auth Proxy, o `gcloud sql export sql` → bucket GCS | `--offload` para reducir impacto en la primaria; `--no-owner --no-privileges` contra `cloudsqladmin` | Auth Proxy a eliminar; conexión directa al VPS como reemplazo |
| Supabase | `pg_dump "$DATABASE_URL" --sslmode=require -Fc --no-owner --no-privileges -f dump.pgc` | `pg_graphql`, `pg_net`, `pgsodium` ausentes en VPS estándar; `pgvector` a instalar manualmente | URL de conexión Supabase → `DATABASE_URL` directo al VPS; Supabase Auth a desacoplar |
Cambio progresivo sin tiempo de inactividad
Instantánea primero — guardar la fuente en un punto en el tiempo
Antes de cualquier operación, tome una instantánea manual de la instancia fuente desde la consola del proveedor (RDS: Actions > Take snapshot; Cloud SQL: Operations > Create backup; Supabase: Settings > Backups > Trigger backup). Esta instantánea es su red de seguridad: permite volver al estado exacto de la base de datos antes de la migración.
pg_dump producción — volcado completo de la base de datos de producción
Lance el volcado usando el comando apropiado para su fuente (ver sección anterior). Anote el horario de inicio y fin del volcado — cualquier transacción confirmada después del inicio del volcado no estará en el archivo. En RDS y Cloud SQL, use el endpoint de lectura si está disponible. Verifique que el archivo dump.pgc no está vacío y completó sin errores:
ls -lh /tmp/dump-*.pgc.pg_restore VPS — restauración y verificación inicial
En el VPS, cree la base de datos de destino y restaure:
createdb -U postgres su_base && pg_restore --no-owner --no-privileges -h localhost -U su_usuario -d su_base -j 4 /tmp/dump-fuente.pgc. Una vez completada la restauración, verifique los conteos de filas en las tablas críticas:SELECT relname, n_live_tup FROM pg_stat_user_tables ORDER BY n_live_tup DESC LIMIT 20;en ambas instancias y compare los números. Una diferencia de pocas filas corresponde a transacciones ocurridas durante el volcado — es esperado.Solo lectura en la fuente — congelar la base de datos de origen
Elija una ventana de cambio (noche, fin de semana, valle de tráfico). Al inicio de esta ventana, coloque la base de datos fuente en modo de solo lectura para detener la brecha entre fuente y destino:
ALTER DATABASE su_base SET default_transaction_read_only = on;. Este paso es el único momento donde una breve pausa de escrituras es inevitable — generalmente dura menos de 30 segundos.Verificar conteos de filas — consistencia antes del cambio
Con la fuente en solo lectura, ejecute una verificación final de conteo de filas tabla por tabla:
psql -h <fuente> -U <usuario> -c 'SELECT count(*) FROM su_tabla_principal;'y compare conpsql -h localhost -U su_usuario -c 'SELECT count(*) FROM su_tabla_principal;'. Si persiste una brecha, ejecute un volcado incremental de los registros creados después del inicio del primer volcado e impórtelos. Continúe hasta la convergencia.Cambiar DATABASE_URL — la aplicación apunta al VPS
Actualice la variable de entorno
DATABASE_URLpara apuntar al VPS:postgresql://su_usuario:su-contraseña@<IP-VPS>:5432/su_base?sslmode=require. Reinicie la aplicación. Verifique que las primeras consultas tienen éxito, que los registros no contienen errores de conexión, y que las escrituras llegan al VPS. La fuente permanece disponible en solo lectura durante otras 24 a 48 horas — no la elimine inmediatamente.
Verificación post-migración
Cuatro verificaciones a realizar en la hora siguiente al cambio.
Conteo de filas. En las diez tablas más voluminosas, compare los conteos de fuente y destino. Una diferencia cero confirma que se capturaron todas las transacciones; una diferencia residual de pocas filas puede indicar transacciones en curso en el momento del cambio.
Sumas de verificación. En las tablas críticas (transacciones, pedidos, datos sensibles), calcule una suma de control: SELECT md5(string_agg(id::text || importe::text, ',' ORDER BY id)) FROM pedidos; y compare el resultado en ambas instancias. Una suma idéntica confirma la integridad de los datos fila a fila.
Prueba de extensiones. Para cada extensión utilizada por la aplicación, ejecute una consulta de prueba mínima: SELECT 'test'::vector(3) para pgvector, SELECT gen_random_uuid() para pgcrypto. Una extensión ausente o mal configurada aparece de inmediato.
Conexiones de la aplicación. Verifique en los registros de la aplicación que las conexiones se establecen en el VPS y no en la base de datos antigua. Verifique también que las conexiones SSL están activas: SELECT ssl, client_addr FROM pg_stat_ssl JOIN pg_stat_activity USING (pid) WHERE client_addr IS NOT NULL LIMIT 5;.
Reversión en menos de 5 minutos. Durante las 24 a 48 horas que siguen al cambio, la fuente permanece viva en solo lectura. Si se detecta un problema bloqueante, la reversión es inmediata: restaure el antiguo DATABASE_URL en la variable de entorno y reinicie la aplicación. Sin pérdida de datos — la fuente no ha sido modificada desde el cambio (está en solo lectura) y el VPS ha registrado las nuevas escrituras desde el cambio. Nunca elimine la fuente antes de 48 horas de estabilidad en el VPS.
Para ir más lejos
Esta guía cubre la migración unidireccional — el procedimiento más común para abandonar un proveedor gestionado. Tres temas complementarios merecen exploración según su contexto.
PostgreSQL autoalojado vs Amazon RDS — una comparación ROI con cifras entre ambos modelos, con los umbrales de tamaño de base de datos y tráfico a partir de los cuales el autoalojamiento se vuelve claramente ventajoso.
Alojar PostgreSQL en un VPS — la configuración detallada post-instalación: postgresql.conf, pg_hba.conf, pgBouncer, monitorización con pg_stat_statements, copias de seguridad automatizadas y endurecimiento de red.
PostgreSQL de alta disponibilidad con Patroni en VPS — si su SLA exige failover automático, Patroni en un clúster de tres nodos (primario + dos réplicas) reproduce las garantías de RDS Multi-AZ o Cloud SQL HA sin el sobrecoste del proveedor.