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
sudoy 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_historyen Flyway,databasechangelogen 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:
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:
psql -h 127.0.0.1 -U migrator -d flyway_demo -c "SELECT current_database();"
current_database
------------------
flyway_demo
(1 row)
Crea un directorio de proyecto con una carpeta para las migraciones de cada herramienta:
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:
nano flyway.env
FLYWAY_URL=jdbc:postgresql://127.0.0.1:5432/flyway_demo
FLYWAY_USER=migrator
FLYWAY_PASSWORD=your_strong_password
chmod 600 flyway.env
Comprueba que la imagen funciona y que conecta con la base de datos:
docker run --rm --network host --env-file flyway.env -v "$PWD/sql:/flyway/sql" flyway/flyway:11 info
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.
Consejoen 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<versión>__<descripción>.sql, con dos guiones bajos. Crea la primera migración:
nano sql/V1__create_customers.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:
nano sql/V2__add_customer_country.sql
ALTER TABLE customers ADD COLUMN country CHAR(2);
CREATE INDEX idx_customers_country ON customers (country);
Aplica las migraciones pendientes:
docker run --rm --network host --env-file flyway.env -v "$PWD/sql:/flyway/sql" flyway/flyway:11 migrate
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:
docker run --rm --network host --env-file flyway.env -v "$PWD/sql:/flyway/sql" flyway/flyway:11 info
+-----------+---------+----------------------+------+---------------------+---------+
| 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:
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
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:
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
undosolo 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:
nano liquibase.env
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
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:
nano changelog/changelog.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:
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:
docker run --rm --network host --env-file liquibase.env -v "$PWD/changelog:/liquibase/changelog" liquibase/liquibase:4.31.1 update
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:
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:
psql -h 127.0.0.1 -U migrator -d liquibase_demo -c "\d orders"
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:
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:
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:
docker run --rm --network host --env-file liquibase.env -v "$PWD/changelog:/liquibase/changelog" liquibase/liquibase:4.31.1 status --verbose
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:
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 refuseddesde el contenedor: falta--network hosto PostgreSQL no escucha en127.0.0.1. Si la base de datos está en otro servidor, usa su IP privada en la URL y revisapg_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 repairactualiza 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 comandorelease-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 enpublicpor 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.
