Self-hosted PostgreSQL migration and backup tool with a local web console and an MCP server.
DBFerry moves a PostgreSQL database from one place to another without hand-writing
pg_dump / pg_restore chains. It takes care of the parts that usually go wrong:
stopping the right services in the right order, keeping a safety backup, verifying the
transfer with SHA-256, restoring into a clean database, and then reporting what actually
happened.
Remote hosts are reached over SSH (Paramiko). The local machine is driven through Git Bash, so the same job definition works whether the target is a server in a datacenter or PostgreSQL installed on the laptop in front of you.
There is no server to deploy and no database of its own — everything runs from the machine you launch it on.
| Key | Direction | What it does |
|---|---|---|
file_to_server |
local .dump → remote host |
Restores a dump file into a Docker PostgreSQL on a remote server |
file_to_local |
local .dump → this machine |
Restores a dump file into the PostgreSQL installed locally |
server_to_server |
live copy | Dumps on the source, pulls it down, restores on the target |
server_to_file |
remote host → local disk | Backs a remote database up to .dump + .sha256 |
local_to_file |
this machine → local disk | Backs a local database up to .dump + .sha256 |
Backup jobs are read-only. Restore jobs can drop and recreate the target database, depending on the chosen restore mode.
- Plan before action. Every job produces a numbered step list first. Nothing is touched until you approve it. The same list is the progress bar during the run.
- Live output. Log lines stream to the console over Server-Sent Events, including the exact commands that were executed and their stdout/stderr.
- Integrity checks. Dumps are written in PostgreSQL custom format (
pg_dump -Fc) and checksummed with SHA-256. A.sha256sidecar file is written next to each backup and verified on the way back in. - Maintenance window handling. Optionally stops the application containers before dumping a live database, then brings them back when the job is done. Container lists are configured per profile.
- Docker or direct. PostgreSQL can live inside a container (commands run through
docker exec) or be installed on the host (commands run directly). Both are supported for remote and local targets. - Safety backup. Before a destructive restore, the current database is dumped to
data/backups/<name>/so there is a way back. - Post-restore verification. Table counts,
schema_migrationsversion and your ownSQL | expectedchecks are validated after the restore. - Profiles and history. Hosts are stored once and reused; every run is appended to a local history file with its status and duration.
- MCP server. The same engine is exposed to AI agents as a Model Context Protocol server: dry-run by default, destructive calls gated behind a confirmation token, and an optional read-only mode that removes restore entirely.
PLAN ──► approve ──► RUN ──► REPORT
│
└── events → console (CLI) / SSE (web)
A live copy is just a pipeline of shell steps, each one logged as it runs:
pg_dump -Fc ──► SHA-256 ──► transfer (SFTP or local copy) ──► DROP + CREATE ──► pg_restore ──► verify
A read-only backup stops at the checksum step and writes the file to disk.
- Python 3.10 or newer (the MCP server SDK requires it)
paramikofor SSH, andmcpfor the AI-agent server- PostgreSQL client tools —
pg_dump,pg_restore,psql,sha256sum- in Docker mode they are expected to exist inside the container
- in direct mode they must be reachable on
PATH(or pointed to with the profile'sPostgreSQL binfield /DBFERRY_PG_BIN)
- For remote jobs: SSH access to the host, and permission to run
dockeron it - Windows only: Git for Windows. Local jobs run
their commands through
bash.exe; the detection can be overridden withDBFERRY_BASH.
git clone https://github.com/AzizARSLN/DBFerry.git
cd DBFerry
python -m pip install -r requirements.txtThe repository is also a standard Python package, so it can be installed into its own
environment with the dbf and dbferry-mcp entry points:
pipx install . # or: uv tool install .
dbf webpython dbf.py webOpens http://127.0.0.1:8787 in your browser. Pick a job type from the tabs, fill in the
connection details, use Planı Göster to preview the steps, then Süreci Başlat to
run it. Everything is logged in the right-hand panel while it runs.
python dbf.py web --port 9000 --no-browserpython dbf.py run --spec job.json # plan, ask for confirmation, run
python dbf.py run --spec job.json --yes # skip the confirmation prompt
python dbf.py inspect --spec job.json # read-only environment check
python dbf.py profiles # list saved hosts
python dbf.py history --limit 20 # recent runsdocker build -t dbferry .
docker run --rm -i -v "$PWD/data:/data" dbferry # MCP server over stdio
docker run --rm -p 8787:8787 -v "$PWD/data:/data" dbferry \
web --host 0.0.0.0 --no-browser # web consoleThe image keeps its state in the /data volume (DBFERRY_DATA_DIR=/data), so profiles and
backups survive between runs. pg_dump / pg_restore still have to exist on the machine
that owns the database — the container drives them over SSH or the local shell.
run and inspect take a JSON file. A backup of a local database looks like this:
{
"type": "local_to_file",
"source_db": "appdb",
"local_db_host": "127.0.0.1",
"local_db_port": 5432,
"local_db_user": "postgres",
"local_db_password": "secret",
"local_pg_mode": "local",
"local_pg_bin": "C:\\Program Files\\PostgreSQL\\17\\bin",
"local_copy_dir": ""
}And a restore onto a remote server:
{
"type": "file_to_server",
"source_dump": "C:\\backups\\appdb.dump",
"target_profile": "a1b2c3d4",
"target_db": "appdb",
"mode": "replace",
"take_safety_backup": true,
"stop_services": true,
"start_services": true,
"verify": true,
"expect_migration": "V068"
}target_profile is the id of a saved profile (see python dbf.py profiles). Leave
local_copy_dir empty to write backups under data/backups/<database>/.
DBFerry also runs as a Model Context Protocol server, so an assistant such as Claude Code, Cursor or Codex can back up, transfer and verify databases through the same plan → approve → run → verify pipeline the console uses.
python dbf.py mcp # stdio (default, for local agents)
python dbf.py mcp --transport streamable-http --port 8765
python dbf.py mcp --transport sse --port 8765The server speaks the same data/ directory as the CLI: profiles are read from
data/profiles.json and job history is appended to data/history.jsonl.
The server is started by the client, so a full clone is not required — point the client at
the mcp subcommand. Claude Desktop / Claude Code (claude_desktop_config.json or
.mcp.json):
{
"mcpServers": {
"dbferry": {
"command": "python",
"args": ["dbf.py", "mcp"],
"cwd": "/absolute/path/to/DBFerry",
"env": { "DBFERRY_DATA_DIR": "/absolute/path/to/DBFerry/data" }
}
}
}Any command that runs python dbf.py mcp works; the cwd (or DBFERRY_DATA_DIR) is what
tells the server which profiles to load.
| Tool | Kind | Purpose |
|---|---|---|
db_profiles |
read | List configured profiles (never returns passwords) |
db_inspect |
read | Environment check: databases, size, table count, migration version, disk |
db_plan |
read | Step list and risk flags for a backup or restore operation |
db_backup |
read | Dump a database to a local .dump + .sha256 |
db_restore |
destructive | Restore a dump into a target; gated by dry-run and a confirmation token |
db_verify |
read | Prove the result: existence, size, tables, migration version, SQL checks |
db_job_status |
read | Progress and recent log lines for running and finished jobs |
The server is built so an agent cannot destroy a database by accident:
- Dry run is the default.
db_restorereturns the step list and executes nothing until it is called withdry_run=false. - Destructive steps need a token. With
mode="replace"the call is rejected unlessconfirmequals the target database name — the agent has to echo the exact name of the database it is about to drop. - Read-only lockdown. Setting
DBFERRY_READONLY=1removesdb_restorefrom the server entirely, leaving only backup and verification tools. - No credentials in arguments. Connection details come from the locally stored profile; passwords are never tool parameters and never returned in tool results.
- Safety backup first. Every restore dumps the current database to
data/backups/<name>/before touching it. - Verified transfers. Dumps are SHA-256 checksummed on write and checked on read.
| Variable | Default | Effect |
|---|---|---|
DBFERRY_DATA_DIR |
./data |
Where profiles, history and backups live |
DBFERRY_READONLY |
unset | 1 disables every data-changing tool |
DBFERRY_PG_BIN |
unset | PostgreSQL bin directory for direct (non-Docker) targets |
DBFERRY_BASH |
auto | bash.exe used for local jobs on Windows |
The official PostgreSQL MCP servers expose a SQL connection: they run queries and let the model reason about a schema. DBFerry's server exposes operations: it plans, performs and verifies a database migration end to end, and it is deliberately unable to serve ad-hoc SQL. Reach for a SQL-oriented server to inspect data; reach for DBFerry to move a database and prove the move succeeded.
DBFerry/
├── dbf.py # CLI entry point (web, mcp, run, inspect, profiles, history)
├── pyproject.toml # packaging metadata and console entry points
├── requirements.txt
├── Dockerfile # container image for the web console and MCP server
└── dbf_ferry/
├── model.py # ServerProfile, JobSpec, job/restore mode definitions
├── store.py # profiles.json + history.jsonl
├── events.py # event bus used by both the CLI and the web console
├── sshx.py # SSH/SFTP layer and host key handling
├── localx.py # local machine layer (drives Git Bash, mirrors the SSH API)
├── pg.py # PostgreSQL operations: dump, restore, verify, docker exec
├── jobs.py # orchestration: plan, run, step tracking, reporting
├── mcp/
│ └── server.py # MCP server: 7 tools over stdio / SSE / streamable HTTP
└── web/
├── app.py # stdlib HTTP server + JSON/SSE API
└── static/ # console UI (index.html, app.js, style.css)
The web console is a thin client over a small JSON API. It is useful for scripting too.
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /api/meta |
version, job types, data directory |
| GET | /api/profiles |
saved hosts (passwords masked) |
| POST | /api/profiles |
create or update a profile |
| POST | /api/profiles/delete |
remove a profile |
| POST | /api/hostkey |
fetch an SSH host key fingerprint |
| POST | /api/local/dbs |
connect to the local PostgreSQL and list databases |
| POST | /api/plan |
build and return the step list for a spec |
| POST | /api/inspect |
start a read-only environment check |
| POST | /api/run |
start a job |
| GET | /api/jobs/<id>/events |
Server-Sent Events stream for a running job |
| POST | /api/jobs/<id>/cancel |
request cancellation |
| GET | /api/history |
recent runs |
- Profiles — including SSH and database passwords — are kept in
data/profiles.jsonon the machine running DBFerry. Passwords are currently stored in plain text there, so treat that file as a secret. It is listed in.gitignoreand is never sent to the browser: the API returns masked profiles withhas_passwordflags instead. - The web console has no authentication and binds to
127.0.0.1by default. Do not expose it to a network you do not control. - In
replacemode a restore drops the target database before recreating it. Leavetake_safety_backupenabled unless you have your own copy. - The MCP server inherits these rules.
db_restoreis dry-run by default, requiresconfirm=<database>forreplace, and disappears entirely whenDBFERRY_READONLY=1.
- Service startup waits for a container to reach
running, nothealthy. Stacks with slow dependencies are reported as soon as the container is up, so give them a moment before pointing traffic at the API. schema_migrationsverification is skipped (with a warning) when the table does not exist in the configured schema.- Local jobs on Windows need Git Bash; on Linux and macOS the system shell is used.
- Environment overrides:
DBFERRY_DATA_DIR,DBFERRY_READONLY,DBFERRY_PG_BIN,DBFERRY_BASH.
Yerel web konsolu ve MCP sunucusu olan, kendi sunucunuzda çalışan PostgreSQL taşıma ve yedekleme aracı.
DBFerry, bir PostgreSQL veritabanını elle pg_dump / pg_restore zinciri yazmadan
başka bir ortama taşır. Genelde sorun çıkaran işleri üstlenir: servisleri doğru sırayla
durdurur, önce güvenlik yedeği alır, aktarımı SHA-256 ile doğrular, temiz bir veritabanına
geri yükler ve sonunda ne yaptığını raporlar.
Uzak makinelere SSH (Paramiko) ile, yerel makineye Git Bash üzerinden erişir. Aynı iş tanımı hem veri merkezindeki bir sunucu hem de yanı başınızdaki PostgreSQL için çalışır. Kurulacak bir sunucu ya da ayrı bir veritabanı yoktur.
| Anahtar | Yön | Açıklama |
|---|---|---|
file_to_server |
yerel .dump → uzak sunucu |
Döküm dosyasını uzaktaki Docker PostgreSQL'e geri yükler |
file_to_local |
yerel .dump → bu bilgisayar |
Döküm dosyasını yereldeki PostgreSQL'e geri yükler |
server_to_server |
canlı kopya | Kaynakta döküm alır, indirir, hedefte geri yükler |
server_to_file |
uzak sunucu → yerel disk | Uzaktaki veritabanını .dump + .sha256 olarak yedekler |
local_to_file |
bu bilgisayar → yerel disk | Yerel veritabanını .dump + .sha256 olarak yedekler |
Yedekleme işleri salt okunurdur; geri yükleme işleri seçilen moda göre hedef veritabanını silip yeniden oluşturabilir.
git clone https://github.com/AzizARSLN/DBFerry.git
cd DBFerry
python -m pip install -r requirements.txtGereksinimler: Python 3.10+, paramiko, mcp ve PostgreSQL istemci araçları (pg_dump,
pg_restore, psql). Windows'ta yerel işlemler için Git for Windows (Git Bash) gerekir.
python dbf.py web # yapılandırma ekranını aç (127.0.0.1:8787)
python dbf.py run --spec job.json # planı göster, onay iste, çalıştır
python dbf.py inspect --spec job.json # salt okunur ortam denetimi
python dbf.py profiles # kayıtlı sunucular
python dbf.py history # iş geçmişi
python dbf.py mcp # MCP sunucusu (stdio) — Claude, Cursor, CodexDBFerry aynı motoru bir Model Context Protocol sunucusu
olarak da açar; ajanlar yedekleme, taşıma ve doğrulama işlerini plan → onay → çalıştır →
doğrula akışıyla yürütür. Sunulan araçlar: db_profiles, db_inspect, db_plan,
db_backup (salt okunur), db_restore (yıkıcı), db_verify, db_job_status.
Güvenlik: db_restore varsayılan olarak yalnızca plan döndürür (dry_run=false gerekir),
replace modunda hedef veritabanı adının birebir onay belirteci olarak verilmesini ister,
DBFERRY_READONLY=1 ayarlandığında sunucudan tamamen kaldırılır. Şifreler araç argümanı
olarak alınmaz; bağlantı bilgileri yerel profillerden okunur.
- Her iş önce plan üretir; siz onaylamadan hiçbir şey değiştirilmez. Aynı liste çalışma sırasında ilerleme göstergesi olarak kullanılır.
- Yedekler PostgreSQL özel formatında (
pg_dump -Fc) alınır ve SHA-256 ile doğrulanır; her yedeğin yanına.sha256dosyası yazılır. Boşlocal_copy_dirbırakılırsa dosyalardata/backups/<veritabanı>/altına gider. - Profiller (
data/profiles.json) şifreleri düz metin olarak tutar; bu dosyayı gizli veri gibi koruyun. Sürüm kontrolüne dahil edilmez ve tarayıcıya maskelenmiş gönderilir. - Web konsolunun kimlik doğrulaması yoktur ve varsayılan olarak
127.0.0.1adresine bağlanır; dışarıya açmayın. replacemodunda hedef veritabanı silinip yeniden oluşturulur; güvenlik yedeğini kapatmadığınızdan emin olun.
Bug reports and pull requests are welcome — please open an issue first for anything larger than a small fix.
Aziz ARSLAN