--- title: "Migraciones de esquema con Flyway y Liquibase en PostgreSQL" description: "Versiona el esquema de PostgreSQL con Flyway y Liquibase en Ubuntu 24.04 con Docker: crea migraciones, aplícalas, revisa el historial y deshaz cambios." category: "Base de Datos Avanzada" language: es source: https://cubepath.com/es/docs/base-de-datos-avanzada/herramientas-de-migracion-de-esquema-flyway-liquibase updated: 2026-09-25 --- # Migraciones de esquema con Flyway y Liquibase en PostgreSQL Cambiar el esquema de una base de datos a mano, con scripts sueltos, acaba en entornos que no coinciden y en despliegues que nadie sabe repetir. Flyway y Liquibase resuelven esto guardando cada cambio como un archivo versionado en el repositorio y registrando en una tabla de la propia base de datos qué cambios se han aplicado. En este tutorial usarás las dos herramientas contra PostgreSQL en Ubuntu 24.04, ejecutándolas con sus imágenes Docker oficiales: crearás migraciones, las aplicarás, consultarás el historial, desharás un cambio con Liquibase y verás cómo integrarlas en un pipeline de CI. ## Requisitos previos - Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath. - Un usuario no root con privilegios `sudo` y permiso para usar Docker. - Docker Engine instalado desde el repositorio oficial. - PostgreSQL 16 instalado en el mismo servidor (`sudo apt install postgresql`). Usar las imágenes Docker evita instalar Java y descargar versiones concretas a mano; en local o en CI el comando es el mismo. ## Conceptos comunes Ambas herramientas funcionan con la misma idea: - Cada cambio de esquema es un archivo inmutable con un identificador. Una vez aplicado en algún entorno no se edita: si hay que corregirlo, se crea un cambio nuevo. - Una tabla de control en la base de datos (`flyway_schema_history` en Flyway, `databasechangelog` en Liquibase) guarda qué cambios se han aplicado, cuándo y con qué checksum. - Al ejecutar la herramienta, compara los archivos con la tabla, aplica solo los pendientes y avisa si un archivo ya aplicado ha cambiado. ## Paso 1: Preparar las bases de datos de prueba Crea un usuario y dos bases de datos, una para cada herramienta, para no mezclar sus tablas de control: ```bash sudo -u postgres psql -c "CREATE ROLE migrator WITH LOGIN PASSWORD 'your_strong_password';" sudo -u postgres createdb -O migrator flyway_demo sudo -u postgres createdb -O migrator liquibase_demo ``` Sustituye `your_strong_password` por una contraseña propia. Comprueba el acceso por TCP, que es como se conectarán los contenedores: ```bash psql -h 127.0.0.1 -U migrator -d flyway_demo -c "SELECT current_database();" ``` ```output current_database ------------------ flyway_demo (1 row) ``` Crea un directorio de proyecto con una carpeta para las migraciones de cada herramienta: ```bash mkdir -p ~/migrations-demo/sql ~/migrations-demo/changelog cd ~/migrations-demo ``` ## Paso 2: Configurar Flyway Flyway lee la conexión de variables de entorno. Guárdalas en un archivo que no subirás al repositorio: ```bash nano flyway.env ``` ```ini FLYWAY_URL=jdbc:postgresql://127.0.0.1:5432/flyway_demo FLYWAY_USER=migrator FLYWAY_PASSWORD=your_strong_password ``` ```bash chmod 600 flyway.env ``` Comprueba que la imagen funciona y que conecta con la base de datos: ```bash docker run --rm --network host --env-file flyway.env -v "$PWD/sql:/flyway/sql" flyway/flyway:11 info ``` ```output Flyway OSS Edition 11.x.y by Redgate Database: jdbc:postgresql://127.0.0.1:5432/flyway_demo (PostgreSQL 16.x) Schema version: << Empty Schema >> +----------+---------+-------------+------+--------------+-------+ | Category | Version | Description | Type | Installed On | State | +----------+---------+-------------+------+--------------+-------+ | No migrations found | +----------+---------+-------------+------+--------------+-------+ ``` `--network host` permite al contenedor llegar a PostgreSQL en `127.0.0.1`. El directorio `sql` del proyecto se monta en `/flyway/sql`, la ubicación por defecto de las migraciones en la imagen. > Consejo: en producción y en CI fija una versión exacta de la imagen (por ejemplo `flyway/flyway:11.x.y`, consultando las etiquetas en Docker Hub) para que todos los entornos usen la misma. ## Paso 3: Escribir y aplicar migraciones con Flyway Flyway identifica las migraciones por el nombre del archivo: `V__.sql`, con dos guiones bajos. Crea la primera migración: ```bash nano sql/V1__create_customers.sql ``` ```sql CREATE TABLE customers ( id BIGSERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE, name TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ``` Crea una segunda migración que añade una columna y un índice: ```bash nano sql/V2__add_customer_country.sql ``` ```sql ALTER TABLE customers ADD COLUMN country CHAR(2); CREATE INDEX idx_customers_country ON customers (country); ``` Aplica las migraciones pendientes: ```bash docker run --rm --network host --env-file flyway.env -v "$PWD/sql:/flyway/sql" flyway/flyway:11 migrate ``` ```output Successfully validated 2 migrations (execution time 00:00.021s) Creating Schema History table "public"."flyway_schema_history" ... Current version of schema "public": << Empty Schema >> Migrating schema "public" to version "1 - create customers" Migrating schema "public" to version "2 - add customer country" Successfully applied 2 migrations to schema "public", now at version v2 (execution time 00:00.045s) ``` En PostgreSQL, Flyway ejecuta cada migración dentro de una transacción: si una sentencia falla, esa migración se revierte completa y no queda a medias. ## Paso 4: Consultar y validar el historial de Flyway Vuelve a ejecutar `info` para ver el estado: ```bash docker run --rm --network host --env-file flyway.env -v "$PWD/sql:/flyway/sql" flyway/flyway:11 info ``` ```output +-----------+---------+----------------------+------+---------------------+---------+ | Category | Version | Description | Type | Installed On | State | +-----------+---------+----------------------+------+---------------------+---------+ | Versioned | 1 | create customers | SQL | 2026-09-25 10:12:03 | Success | | Versioned | 2 | add customer country | SQL | 2026-09-25 10:12:03 | Success | +-----------+---------+----------------------+------+---------------------+---------+ ``` Comprueba qué pasa si alguien modifica una migración ya aplicada. Añade un comentario al final de `sql/V1__create_customers.sql` y ejecuta `validate`: ```bash echo "-- cambio accidental" >> sql/V1__create_customers.sql docker run --rm --network host --env-file flyway.env -v "$PWD/sql:/flyway/sql" flyway/flyway:11 validate ``` ```output ERROR: Validate failed: Migrations have failed validation Migration checksum mismatch for migration version 1 -> Applied to database : 1234567890 -> Resolved locally : -987654321 ``` Flyway se niega a continuar porque el archivo ya no coincide con lo aplicado. Deshaz el cambio borrando la última línea del archivo y la validación volverá a pasar: ```bash sed -i '$d' sql/V1__create_customers.sql ``` `migrate` hace esta misma validación antes de aplicar nada. Dos detalles importantes de Flyway en su edición gratuita: - No hay deshacer automático. El comando `undo` solo existe en las ediciones de pago; para revertir un cambio se escribe una migración nueva (`V3__drop_customer_country.sql`) que lo deshaga. - El comando `clean`, que borra todo el esquema, está desactivado por defecto (`cleanDisabled=true`). Déjalo así en cualquier entorno con datos reales. ## Paso 5: Configurar Liquibase Liquibase también acepta la conexión por variables de entorno. Crea su archivo: ```bash nano liquibase.env ``` ```ini LIQUIBASE_COMMAND_URL=jdbc:postgresql://127.0.0.1:5432/liquibase_demo LIQUIBASE_COMMAND_USERNAME=migrator LIQUIBASE_COMMAND_PASSWORD=your_strong_password LIQUIBASE_COMMAND_CHANGELOG_FILE=changelog/changelog.sql ``` ```bash chmod 600 liquibase.env ``` En esta guía se usa la imagen `liquibase/liquibase:4.31.1`, que incluye el driver JDBC de PostgreSQL. Antes de pasar a la rama 5.x revisa sus notas de versión, porque cambió el empaquetado de drivers y extensiones. ## Paso 6: Escribir un changelog con rollback Liquibase organiza los cambios en un changelog, que puede escribirse en XML, YAML, JSON o SQL. El formato SQL con anotaciones es el más fácil de leer: cada `--changeset autor:id` define un cambio y `--rollback` indica cómo deshacerlo: ```bash nano changelog/changelog.sql ``` ```sql --liquibase formatted sql --changeset your_name:1 CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, customer_id BIGINT NOT NULL, total_cents INTEGER NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); --rollback DROP TABLE orders; --changeset your_name:2 ALTER TABLE orders ADD COLUMN status TEXT NOT NULL DEFAULT 'pending'; --rollback ALTER TABLE orders DROP COLUMN status; ``` Antes de aplicar nada, revisa el SQL exacto que Liquibase ejecutaría: ```bash docker run --rm --network host --env-file liquibase.env -v "$PWD/changelog:/liquibase/changelog" liquibase/liquibase:4.31.1 update-sql ``` La salida incluye la creación de las tablas de control `databasechangelog` y `databasechangeloglock`, las sentencias de los dos changesets y los `INSERT` en el historial. Es una buena práctica revisar esta salida en las revisiones de código antes de ir a producción. ## Paso 7: Aplicar y consultar cambios con Liquibase Aplica el changelog: ```bash docker run --rm --network host --env-file liquibase.env -v "$PWD/changelog:/liquibase/changelog" liquibase/liquibase:4.31.1 update ``` ```output Running Changeset: changelog/changelog.sql::1::your_name Running Changeset: changelog/changelog.sql::2::your_name UPDATE SUMMARY Run: 2 Previously run: 0 Filtered out: 0 ------------------------------- Total change sets: 2 Liquibase command 'update' was executed successfully. ``` Consulta el historial: ```bash docker run --rm --network host --env-file liquibase.env -v "$PWD/changelog:/liquibase/changelog" liquibase/liquibase:4.31.1 history ``` Y confírmalo desde PostgreSQL: ```bash psql -h 127.0.0.1 -U migrator -d liquibase_demo -c "\d orders" ``` ```output Table "public.orders" Column | Type | Collation | Nullable | Default -------------+--------------------------+-----------+----------+------------------------------------ id | bigint | | not null | nextval('orders_id_seq'::regclass) customer_id | bigint | | not null | total_cents | integer | | not null | created_at | timestamp with time zone | | not null | now() status | text | | not null | 'pending'::text ``` ## Paso 8: Deshacer un cambio con Liquibase Gracias a las líneas `--rollback`, Liquibase puede revertir changesets. Previsualiza primero el SQL de deshacer el último: ```bash docker run --rm --network host --env-file liquibase.env -v "$PWD/changelog:/liquibase/changelog" liquibase/liquibase:4.31.1 rollback-count-sql --count=1 ``` Si es lo esperado, ejecútalo: ```bash docker run --rm --network host --env-file liquibase.env -v "$PWD/changelog:/liquibase/changelog" liquibase/liquibase:4.31.1 rollback-count --count=1 ``` Comprueba que la columna `status` ha desaparecido con `\d orders` y que el changeset 2 vuelve a figurar como pendiente: ```bash docker run --rm --network host --env-file liquibase.env -v "$PWD/changelog:/liquibase/changelog" liquibase/liquibase:4.31.1 status --verbose ``` ```output 1 changeset has not been applied to migrator@jdbc:postgresql://127.0.0.1:5432/liquibase_demo changelog/changelog.sql::2::your_name ``` Para volver a un punto conocido en producción, marca cada despliegue con `tag --tag=v1.0` después de `update` y usa `rollback --tag=v1.0` si necesitas volver a él. Ten en cuenta que un rollback que borra columnas o tablas también borra sus datos: en producción suele ser más seguro desplegar un cambio nuevo que corrija el anterior. ## Paso 9: Ejecutar las migraciones en CI El flujo habitual es que el pipeline aplique las migraciones antes de desplegar la nueva versión de la aplicación, con credenciales guardadas como secretos. Un ejemplo con GitHub Actions para Flyway: ```yaml name: migrate on: push: branches: [main] jobs: migrate: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Apply database migrations env: FLYWAY_URL: ${{ secrets.FLYWAY_URL }} FLYWAY_USER: ${{ secrets.FLYWAY_USER }} FLYWAY_PASSWORD: ${{ secrets.FLYWAY_PASSWORD }} run: | docker run --rm \ -e FLYWAY_URL -e FLYWAY_USER -e FLYWAY_PASSWORD \ -v "$PWD/sql:/flyway/sql" \ flyway/flyway:11 migrate ``` El runner necesita alcanzar la base de datos, así que en la práctica este trabajo suele ejecutarse en un runner propio dentro de tu red privada. Con Liquibase el paso es equivalente, cambiando la imagen, las variables `LIQUIBASE_COMMAND_*` y el comando `update`. ## Flyway o Liquibase: cuál elegir | Aspecto | Flyway | Liquibase | |---------|--------|-----------| | Formato de los cambios | SQL (y Java) | SQL, XML, YAML o JSON | | Identificación | Versión en el nombre del archivo | `autor:id` de cada changeset | | Rollback | Solo en ediciones de pago (`undo`) | Incluido en la edición gratuita, si defines el rollback | | Curva de aprendizaje | Muy baja | Algo mayor | | Varios motores con el mismo changelog | No, el SQL es específico | Sí, con los formatos XML/YAML y sus change types | Elige **Flyway** si tu equipo prefiere escribir SQL puro y revertir con migraciones nuevas: es más simple y hay menos que aprender. Elige **Liquibase** si necesitas rollback integrado, previsualizar el SQL en revisiones o mantener un mismo changelog para varios motores de base de datos. Lo importante es elegir una y usarla para todos los cambios de esquema, sin excepciones. ## Solución de problemas - **`Connection refused` desde el contenedor**: falta `--network host` o PostgreSQL no escucha en `127.0.0.1`. Si la base de datos está en otro servidor, usa su IP privada en la URL y revisa `pg_hba.conf`. - **`Validate failed: Migration checksum mismatch`**: se modificó una migración ya aplicada. Restaura el archivo original; si el cambio era intencionado y equivalente, `flyway repair` actualiza el checksum. - **Liquibase se queda esperando el bloqueo (`Waiting for changelog lock`)**: una ejecución anterior se interrumpió. Si estás seguro de que no hay otra en curso, libera el bloqueo con el comando `release-locks`. - **`permission denied for schema public`**: el usuario no es propietario de la base de datos. En PostgreSQL 15 y posteriores solo el propietario puede crear objetos en `public` por defecto. ## Conclusión Has aplicado migraciones versionadas en PostgreSQL con Flyway y con Liquibase, has visto cómo detectan cambios en archivos ya aplicados y cómo Liquibase revierte changesets con rollback definido. Como siguientes pasos, guarda el directorio de migraciones en el mismo repositorio que la aplicación, añade el paso de migración a tu pipeline de despliegue y prueba cada migración contra una copia reciente de la base de datos de producción antes de aplicarla.