- Python 100%
Maintenance policy going forward. Admins must update Pleroma to the
latest stable release and run its ecto migrations before migrating
(new step 1 1/2). Mitra must include Pleroma password-digest support,
which was merged upstream after the v5.10.0 release cut — so a
release newer than v5.10.0 (or main) is required; the old step 2
(build with companion patch) is now the fallback for v5.10.0 exactly.
💘 Generated with Crush
Assisted-by: Crush:glm-5.3
|
||
|---|---|---|
| .gitignore | ||
| pleroma2mitra.diff | ||
| pleroma2mitra.py | ||
| README.md | ||
| test.py | ||
Pleroma → Mitra Migration
ACHTUNG! THIS IS 100% SLOPCODE. NO WARRANTIES.
Supported and tested with: latest pleroma stable (stable branch, 2.10.2) → latest mitra stable or newer (password-digest support merged upstream after v5.10.0). Last verified: see ../AGENTS.md "Upstream release testing" / state/tested_releases.json.
What gets migrated
| Data | Notes |
|---|---|
| Local users | Passwords hashes kept (PBKDF2 digests; Mitra needs a patch, see step 2), users don't need to reset passwords |
| Remote actors | Per --remote-scope (default interacted): authors of remote posts your users replied to, liked or boosted — or ALL remote users Pleroma has seen (all). Full profile: name, bio, fields, avatar/banner as proxy links, RSA public key; actors without a usable key get updated_at backdated so Mitra re-fetches them within a day |
| Posts | ALL local posts + remote posts per --remote-scope; IDs rewritten to UUIDv7 derived from created_at so Mitra's id DESC timelines order correctly |
| Follows, blocks, mutes, bookmarks | Follow counters (follower_count/following_count) are recomputed from the actual follow rows after migration |
| Conversations / DMs | |
| Media attachments | Local files become File rows; remote attachments become proxy Link rows (see below) |
| Avatars & banners | Local users only |
| Emojis | Local (files copied) + remote (link rows); post and profile emoji links |
Not migrated: notifications, reports, OAuth apps/tokens, lists, user settings/preferences, instance policies. Mitra starts with a fresh set of those.
How much remote content is migrated depends on --remote-scope.
The default (interacted) migrates only the remote posts your users
replied to, liked or boosted, plus their authors — fast and small.
--remote-scope all copies every remote actor and every remote post
stored in the source database; on a busy instance that is a lot of data
(hundreds of thousands of actors, millions of posts) and takes hours.
Behavior worth knowing before you start
- Remote media attachments are proxied, not stored.
Mitra caches remote attachments by default, which isn't an issue since it has good tools for pruning. For a migration, it means we would have to download every remote attachment pleroma has seen so mitra can reference the file in the db for each remote attachment and show it.
Another way of doing this is by referencing a url instead of a local file
in the db for remote posts. This is what happens when you set a ProxyMedia
filter. So this is what we do. Unless you set a proxy rule mitra will
cache new remote attachments, and the old ones in the old pleroma install
will continue to be url-only as if they were media proxied.
- Local media files must exist on disk.
The migrator creates File-type rows that point at <storage_dir>/media/<file>.
Use --uploads-dir (step 8) to have the migrator copy them from Pleroma's
uploads directory; missing files are skipped with a warning and the attachment/avatar
stays unset.
If you want to do this yourself, all you need to do is flatten the pleroma
uploads directory (pleroma is sharded, mitra is flat).
-
Attachment limits follow Mitra's hardcoded source ceilings. Neither the per-post attachment count nor the media type is filtered: Pleroma lets users upload any file type, and Mitra's
attachment_limit/ supported-media-types list are configurable values with no hardcoded source ceiling — filtering at the defaults (16 / built-in type list) would silently drop data on instances whose admins raised them. The limits that ARE hardcoded in Mitra's source are still applied: media URL ≤ 2000 chars, description ≤ 3000 chars. Post length is never enforced (Mitra's backend has no character limit, only a 150000-byte size check). -
.jpg/.jpegemojis are migrated as-is, but Mitra's own upload path only accepts gif/png/webp, so those can't be re-uploaded through the Mitra UI without converting. -
Destructive:
--reset-target-db/--fullDROP the target database. Point--target-dsnonly at a database you can afford to lose.
Step-by-step migration guide
Step 0 — Requirements
- Python ≥ 3.11 (
tomllibis used to read the Mitra config) with theasyncpgpackage (pip install asyncpg). If your Mitra config is YAML instead of TOML, alsopip install pyyaml. - PostgreSQL reachable for both databases (URL-style DSNs:
postgres://user[:password]@host:port/db). - Mitra built from source, at a release newer than v5.10.0 (for
Pleroma password-digest support, see step 2), and its binary
reachable (
--mitra-binary, default:mitrain$PATH). - Read access to the Pleroma database; write access to the Mitra database.
- Disk space in Mitra's
storage_dirfor your local uploads + emoji files. - See helper menu with
python3 pleroma2mitra.py -hor cli reference at the bottom of this page.
Step 1 — Stop Pleroma and back up
Stop Pleroma so the database doesn't change during migration, then take a backup you trust:
systemctl stop pleroma # or however you run it
pg_dump -Fc -f pleroma-backup.dump postgres://pleroma@127.0.0.1:5432/pleroma
Step 1½ — Update Pleroma to the latest stable release
Supported combo: latest Pleroma stable → latest Mitra stable. Before migrating, update your Pleroma instance to the latest stable release and run its database migrations, so the migrator sees the schema it was tested against:
# after deploying the new Pleroma release files:
su pleroma -s $SHELL -lc "MIX_ENV=prod mix ecto.migrate"
Then take note of your pleroma version — if it is newer than the
one this migrator was last tested with (see the compatibility table
at the top), re-check the release testing notes or
open an issue.
Step 2 — Mitra version
Mitra must understand Pleroma's $pbkdf2-sha512$ password digests so
users can log in unchanged. This support was merged upstream after
the v5.10.0 release — use a Mitra release newer than v5.10.0 (or
current main):
git clone https://codeberg.org/silverpill/mitra
cd mitra
git checkout <newest-release-tag> # must include the Pleroma digest support
cargo build --release --features production # see mitra's readme
# binary: target/release/mitra
If you are stuck on v5.10.0 exactly, apply the historical companion
patch (pleroma2mitra.diff, alongside pleroma2mitra.py):
git apply /path/to/pleroma2mitra.diff.
Without Pleroma-digest support, migrated users cannot log in with their old passwords.
Step 3 — Create the Mitra database
Create a role and database for Mitra (the migrator's
--reset-target-db can drop/recreate the database itself, but the role
must exist):
CREATE ROLE mitra WITH LOGIN PASSWORD 'secret';
CREATE DATABASE mitra OWNER mitra ENCODING 'UTF8';
Step 4 — Write the Mitra config
Create config.toml. The fields the migrator reads are marked:
database_url = "postgres://mitra:secret@127.0.0.1:5432/mitra" # must equal --target-dsn
storage_dir = "/var/lib/mitra" # media goes to <storage_dir>/media
instance_url = "https://your.domain" # MUST be your Pleroma domain
# ... the rest of Mitra's config (see Mitra's config.example.toml)
Important:
instance_urlmust be the same domain your Pleroma instance used (https://cum.salonin our case). Local posts, media URLs and webfinger addresses are migrated with that domain baked in.--target-dsnmust matchdatabase_urlexactly.
Step 5 — Locate Pleroma's emoji and uploads directories
- Emoji dir (
--emoji-dir): the directory containing the emoji packs —<static_dir>/emojiin a default install, usually/var/lib/pleroma/instance/static/emoji. The URLhttps://<domain>/emoji/<path>maps directly to<emoji-dir>/<path>. - Uploads dir (
--uploads-dir): Pleroma's local uploader directory,uploadsinside Pleroma's working directory by default (e.g./var/lib/pleroma/uploads). Files are content-addressed, optionally sharded (aa/bb/cc/<sha256>.<ext>); the migrator handles both layouts and flattens them into Mitra's media dir.
Both flags are optional: omitting --emoji-dir skips emoji migration
(with a warning); omitting --uploads-dir skips the file copy (files
must then already be in <storage_dir>/media/).
Step 6 — Dry run (recommended)
Validate connectivity and config without touching the target:
python3 pleroma2mitra.py \
--source-dsn postgres://pleroma@127.0.0.1:5432/pleroma \
--target-dsn postgres://mitra@127.0.0.1:5432/mitra \
--config-file /etc/mitra/config.toml \
--dry-run
--dry-run cannot be combined with the orchestration flags
(--reset-target-db, --run-schema-migrations).
Step 7 — Run the migration
python3 pleroma2mitra.py \
--full \
--source-dsn postgres://pleroma@127.0.0.1:5432/pleroma \
--target-dsn postgres://mitra@127.0.0.1:5432/mitra \
--config-file /etc/mitra/config.toml \
--emoji-dir /var/lib/pleroma/instance/static/emoji \
--uploads-dir /var/lib/pleroma/uploads \
2>&1 | tee migrate.log
--full = --reset-target-db + --run-schema-migrations + migrate,
in that order:
- Stop any running
mitraprocesses (so any running server doesn't lose its database or hold the HTTP port). - DROP and recreate the target database. You are asked to type
the database name to confirm (
--assume-yesskips the prompt — use it in scripts). - Run Mitra's schema migrations (
mitra instance-report, which applies pending migrations on startup). - Migrate the data (phases 1–8: users, remote actors, posts, follows, blocks/mutes, bookmarks, media, emojis), then print post-migration row counts.
Prefer to run the steps yourself? Use the individual flags instead of
--full. If your mitra binary isn't in $PATH, pass
--mitra-binary /path/to/mitra.
Step 7½ — Start the server
The migrator does not start the Mitra server. Once the migration finishes, start it yourself the way you normally deploy Mitra, e.g.:
CONFIG_PATH=/etc/mitra/config.toml /usr/local/bin/mitra server
The first requests may be slower while caches warm up.
Remote content scope: by default (--remote-scope interacted) only
remote posts your users replied to, liked or boosted are migrated, plus
their authors. Pass --remote-scope all to migrate every remote actor
and post the source database has seen — much slower (hours instead of
minutes on a busy instance) and much bigger.
The migration is resumable for the data phases (inserts use
ON CONFLICT DO NOTHING and deterministic IDs), but the supported
path for a re-run is --full from scratch. On our ~316k-post instance
the default (interacted) run takes about 14 minutes; --remote-scope all (10M+ posts) took about 3¼ hours.
Step 8 — What happens to your files
- Local media (
--uploads-dir): every local file referenced by migrated content (avatars, banners, post attachments) is copied from Pleroma's uploads dir into<storage_dir>/media/, flattened to<storage_dir>/media/<sha256>.<ext>. Files already present are kept; files missing from the uploads dir are skipped with a warning (the attachment stays unset). - Local emojis (
--emoji-dir): copied into the media dir content-addressed (<sha256-of-content>.<ext>).
Step 9 — Verify
-
Check the row counts printed at the end of the migration against your expectations (
post,actor_profile,user_account,media_attachment,emoji, ...). -
Log in through the API or a client with an existing Pleroma account — the old password must work (patch from step 2). You might need to clear localstorage/cookies in your browser.
-
Spot-check timelines, a profile with avatar/banner, a post with an attachment, and an emoji.
-
This repo also ships
test.py, an end-to-end check of the migrated instance:python3 test.py --base-url http://127.0.0.1:8380 \ --user <a-migrated-local-user> --pass '<password>'It verifies login, timelines, local/remote posts and profiles, local media serving, bookmarks, favourites, conversations, search and follow lists, and prints a report with an error count.
Step 10 — After the migration
- Federation: the migrator copies each remote actor's RSA public
key into Mitra's format, so incoming signed activities verify
immediately for the vast majority of actors. The few actors without
a usable key are imported with empty
public_keysandupdated_atbackdated by 2 days, so Mitra's actor-refresh picks them up immediately; until refreshed, incoming activities from those actors fail signature checks. This resolves itself within about a day. - Web UI: Mitra has no built-in web client. If you want one, build
mitra-web separately and point
web_client_dirat itsdist/. When building, setVITE_BACKEND_URLcorrectly (or empty, for same-origin) in the web client's.env.local, otherwise the bundle hardcodes the wrong origin for media URLs. - Backups: take a fresh
pg_dumpof the Mitra database and back up<storage_dir>once you're satisfied. - Keep the Pleroma database around until you've fully signed off — the password digests are unchanged, so migrating back is possible.
CLI reference (pleroma2mitra.py)
Required:
--source-dsn— Pleroma DB, URL-style DSN.--target-dsn— Mitra DB, URL-style DSN (must equaldatabase_urlin the config).--config-file— Mitra config (TOML or YAML); used forstorage_dir(media),http_host/http_port(server startup).
Data flags:
--emoji-dir PATH— migrate emojis (local files copied, remote as link rows, post + profile links). Omit to skip; bad path fails fast.--uploads-dir PATH— copy referenced local media files into Mitra's media dir. Omit if already in place; bad path fails fast.--remote-scope interacted|all— which remote actors/posts to migrate.interacted(default): only remote posts local users replied to, liked or boosted, plus their authors.all: every remote actor and post the source DB has seen (much slower and bigger).--skip-users,--skip-remote-actors,--skip-posts,--skip-media— skip individual phases (--skip-mediaalso skips the--uploads-dircopy;--skip-postsalso skips emojis).--dry-run— validate without writing.--quiet— suppress progress output.
Orchestration flags (ex-migrate.sh):
--reset-target-db— stop running mitra, DROP + recreate target DB (confirmation prompt;--assume-yesto skip).--run-schema-migrations— apply Mitra's migrations first.--full— all of the above, in order.--mitra-binary PATH— mitra binary for schema migrations / server (defaultmitrafrom$PATH).
The local domain is auto-detected from the source database (first local user's AP ID host).