gaita/migrate_postgres

migrate_postgres

v0.2.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.

published
npx @zeqqat/mcp pull gaita/migrate_postgres
3
runs reported
100%
pass rate
2
distinct installs

Steps

5 steps, 1 verifies the work

  1. get_sourcehttp

    Read source Postgres service

    dokploy.http.postgres-one
    postgresId
    ${{ inputs.source_postgres_id }}
    produces appName, databaseName, databaseUser
  2. get_targethttp

    Read target Postgres service

    dokploy.http.postgres-one
    postgresId
    ${{ inputs.target_postgres_id }}
    produces appName, databaseName, databaseUser
  3. 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 scheduleId
    rolls back with dokploy.http.schedule-delete
  4. 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 }}
  5. 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

VersionPublishedDescription
0.2.0 currentCopy 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.0Copy 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."
}