gaita/migrate_postgres
migrate_postgres
v0.2.0pinned versionCopy one Dokploy Postgres service into another and verify the copy by content checksum, entirely through the Dokploy REST API. The dump, restore, source and target checksums, and the comparison all run inside a one-off Dokploy schedule on the Dokploy server next to the databases, so the workflow needs no direct Postgres connection, no external port, and no tunnel. schedule.runManually blocks until the script finishes and returns HTTP 500 if it exits non-zero, so a checksum mismatch or a failed restore fails the run_migration step, which deletes the schedule via compensation. Safe to retry.
npx @zeqqat/mcp pull gaita/[email protected]Steps
5 steps, 1 verifies the work
- get_sourcehttp
Read source Postgres service
dokploy.http.postgres-one- postgresId
- ${{ inputs.source_postgres_id }}
produces appName, databaseName, databaseUser - get_targethttp
Read target Postgres service
dokploy.http.postgres-one- postgresId
- ${{ inputs.target_postgres_id }}
produces appName, databaseName, databaseUser - create_schedulehttp
Create the migration+verify schedule
dokploy.http.schedule-create- name
- zeqqat-migrate-${{ inputs.target_postgres_id }}
- script
- set -eo pipefail SRC=$(docker ps -q -f name=${{ steps.get_source.outputs.appName }} | head -1) TGT=$(docker ps -q -f name=${{ steps.get_target.outputs.appName }} | head -1) if [ -z "$SRC" ] || [ -z "$TGT" ]; then echo "missing container" >&2; exit 1; fi cat > /tmp/zeqqat_checksum.sql <<'ZEQQAT_CHK_SQL' SELECT coalesce(md5(string_agg(tbl||':'||n||':'||h, ',' ORDER BY tbl)), 'empty') AS checksum FROM ( SELECT c.relname AS tbl, (xpath('/row/h/text()', query_to_xml(format('SELECT coalesce(md5(string_agg(md5(x.*::text), '','' ORDER BY md5(x.*::text))), ''empty'') AS h FROM %I.%I x', n.nspname, c.relname), false, true, '')))[1]::text AS h, (xpath('/row/n/text()', query_to_xml(format('SELECT count(*) AS n FROM %I.%I', n.nspname, c.relname), false, true, '')))[1]::text AS n FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'r' AND n.nspname = 'public' ) s; ZEQQAT_CHK_SQL SRCSUM=$(docker exec -i "$SRC" psql -U ${{ steps.get_source.outputs.databaseUser }} -d ${{ steps.get_source.outputs.databaseName }} -tAX -v ON_ERROR_STOP=1 < /tmp/zeqqat_checksum.sql) docker exec "$SRC" pg_dump -U ${{ steps.get_source.outputs.databaseUser }} -d ${{ steps.get_source.outputs.databaseName }} --no-owner --no-privileges --clean --if-exists | docker exec -i "$TGT" psql -U ${{ steps.get_target.outputs.databaseUser }} -d ${{ steps.get_target.outputs.databaseName }} -v ON_ERROR_STOP=1 -q TGTSUM=$(docker exec -i "$TGT" psql -U ${{ steps.get_target.outputs.databaseUser }} -d ${{ steps.get_target.outputs.databaseName }} -tAX -v ON_ERROR_STOP=1 < /tmp/zeqqat_checksum.sql) if [ "$SRCSUM" != "$TGTSUM" ]; then echo "checksum mismatch: source=$SRCSUM target=$TGTSUM" >&2; exit 1; fi echo "migration verified: source=$SRCSUM target=$TGTSUM"
- command
- noop
- enabled
- true
- shellType
- bash
- scheduleType
- dokploy-server
- cronExpression
- 0 0 31 12 *
produces scheduleIdrolls back with dokploy.http.schedule-delete - run_migrationhttpverification
Fire the migration schedule (blocks; non-zero exit returns HTTP 500 and fails this step)
dokploy.http.schedule-runManually- scheduleId
- ${{ steps.create_schedule.outputs.scheduleId }}
- cleanuphttp
Delete the migration schedule
dokploy.http.schedule-delete- scheduleId
- ${{ steps.create_schedule.outputs.scheduleId }}
On failure
One step declares how to undo itself. If a later step fails, the executor rolls these back in reverse order and reports that the state is clean.
- create_schedulerolls back withdokploy.http.schedule-delete
Versions
| Version | Published | Description |
|---|---|---|
| 0.2.0 current | Copy one Dokploy Postgres service into another and verify the copy by content checksum, entirely through the Dokploy REST API. The dump, restore, source and target checksums, and the comparison all run inside a one-off Dokploy schedule on the Dokploy server next to the databases, so the workflow needs no direct Postgres connection, no external port, and no tunnel. schedule.runManually blocks until the script finishes and returns HTTP 500 if it exits non-zero, so a checksum mismatch or a failed restore fails the run_migration step, which deletes the schedule via compensation. Safe to retry. | |
| 0.1.0 | Copy one Dokploy Postgres service into another and verify the copy by content checksum, entirely through the Dokploy REST API. The dump, restore, source and target checksums, and the comparison all run inside a one-off Dokploy schedule on the Dokploy server next to the databases, so the workflow needs no direct Postgres connection, no external port, and no tunnel. schedule.runManually blocks until the script finishes and returns HTTP 500 if it exits non-zero, so a checksum mismatch or a failed restore fails the run_migration step, which deletes the schedule via compensation. Safe to retry. |
Raw workflow JSON
{
"name": "migrate_postgres",
"tags": [
"dokploy",
"postgres",
"migration",
"backup"
],
"steps": [
{
"id": "get_source",
"name": "Read source Postgres service",
"type": "http",
"calls": "dokploy.http.postgres-one",
"inputs": {
"postgresId": "${{ inputs.source_postgres_id }}"
},
"outputs": {
"appName": {
"type": "string",
"required": true
},
"databaseName": {
"type": "string",
"required": true
},
"databaseUser": {
"type": "string",
"required": true
}
}
},
{
"id": "get_target",
"name": "Read target Postgres service",
"type": "http",
"calls": "dokploy.http.postgres-one",
"inputs": {
"postgresId": "${{ inputs.target_postgres_id }}"
},
"outputs": {
"appName": {
"type": "string",
"required": true
},
"databaseName": {
"type": "string",
"required": true
},
"databaseUser": {
"type": "string",
"required": true
}
}
},
{
"id": "create_schedule",
"name": "Create the migration+verify schedule",
"type": "http",
"calls": "dokploy.http.schedule-create",
"inputs": {
"name": "zeqqat-migrate-${{ inputs.target_postgres_id }}",
"script": "set -eo pipefail\nSRC=$(docker ps -q -f name=${{ steps.get_source.outputs.appName }} | head -1)\nTGT=$(docker ps -q -f name=${{ steps.get_target.outputs.appName }} | head -1)\nif [ -z \"$SRC\" ] || [ -z \"$TGT\" ]; then echo \"missing container\" >&2; exit 1; fi\ncat > /tmp/zeqqat_checksum.sql <<'ZEQQAT_CHK_SQL'\nSELECT coalesce(md5(string_agg(tbl||':'||n||':'||h, ',' ORDER BY tbl)), 'empty') AS checksum FROM (\n SELECT c.relname AS tbl,\n (xpath('/row/h/text()', query_to_xml(format('SELECT coalesce(md5(string_agg(md5(x.*::text), '','' ORDER BY md5(x.*::text))), ''empty'') AS h FROM %I.%I x', n.nspname, c.relname), false, true, '')))[1]::text AS h,\n (xpath('/row/n/text()', query_to_xml(format('SELECT count(*) AS n FROM %I.%I', n.nspname, c.relname), false, true, '')))[1]::text AS n\n FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE c.relkind = 'r' AND n.nspname = 'public'\n) s;\nZEQQAT_CHK_SQL\nSRCSUM=$(docker exec -i \"$SRC\" psql -U ${{ steps.get_source.outputs.databaseUser }} -d ${{ steps.get_source.outputs.databaseName }} -tAX -v ON_ERROR_STOP=1 < /tmp/zeqqat_checksum.sql)\ndocker exec \"$SRC\" pg_dump -U ${{ steps.get_source.outputs.databaseUser }} -d ${{ steps.get_source.outputs.databaseName }} --no-owner --no-privileges --clean --if-exists | docker exec -i \"$TGT\" psql -U ${{ steps.get_target.outputs.databaseUser }} -d ${{ steps.get_target.outputs.databaseName }} -v ON_ERROR_STOP=1 -q\nTGTSUM=$(docker exec -i \"$TGT\" psql -U ${{ steps.get_target.outputs.databaseUser }} -d ${{ steps.get_target.outputs.databaseName }} -tAX -v ON_ERROR_STOP=1 < /tmp/zeqqat_checksum.sql)\nif [ \"$SRCSUM\" != \"$TGTSUM\" ]; then echo \"checksum mismatch: source=$SRCSUM target=$TGTSUM\" >&2; exit 1; fi\necho \"migration verified: source=$SRCSUM target=$TGTSUM\"",
"command": "noop",
"enabled": true,
"shellType": "bash",
"scheduleType": "dokploy-server",
"cronExpression": "0 0 31 12 *"
},
"outputs": {
"scheduleId": {
"type": "string",
"required": true
}
},
"compensate": {
"calls": "dokploy.http.schedule-delete",
"inputs": {
"scheduleId": "${{ steps.create_schedule.outputs.scheduleId }}"
}
}
},
{
"id": "run_migration",
"name": "Fire the migration schedule (blocks; non-zero exit returns HTTP 500 and fails this step)",
"role": "verification",
"type": "http",
"calls": "dokploy.http.schedule-runManually",
"inputs": {
"scheduleId": "${{ steps.create_schedule.outputs.scheduleId }}"
},
"outputs": {}
},
{
"id": "cleanup",
"name": "Delete the migration schedule",
"type": "http",
"calls": "dokploy.http.schedule-delete",
"inputs": {
"scheduleId": "${{ steps.create_schedule.outputs.scheduleId }}"
},
"outputs": {}
}
],
"inputs": {
"source_postgres_id": {
"type": "string",
"required": true
},
"target_postgres_id": {
"type": "string",
"required": true
}
},
"defaults": {
"retries": {
"backoff": "fixed",
"base_ms": 0,
"attempts": 1
}
},
"requires": [
"dokploy"
],
"on_failure": "cascade",
"description": "Copy one Dokploy Postgres service into another and verify the copy by content checksum, entirely through the Dokploy REST API. The dump, restore, source and target checksums, and the comparison all run inside a one-off Dokploy schedule on the Dokploy server next to the databases, so the workflow needs no direct Postgres connection, no external port, and no tunnel. schedule.runManually blocks until the script finishes and returns HTTP 500 if it exits non-zero, so a checksum mismatch or a failed restore fails the run_migration step, which deletes the schedule via compensation. Safe to retry."
}