Migrate your pleroma instance to mitra!
Find a file
perndog ba5f678675 Document supported versions: latest Pleroma stable to latest Mitra stable
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
2026-09-12 02:53:36 -06:00
.gitignore .gitignore 2026-08-04 08:19:08 -06:00
pleroma2mitra.diff comments? y 2026-08-25 13:14:28 -06:00
pleroma2mitra.py Use Mitra's hardcoded source ceilings for attachment limits 2026-09-11 16:59:56 -06:00
README.md Document supported versions: latest Pleroma stable to latest Mitra stable 2026-09-12 02:53:36 -06:00
test.py fix media migration: TOML config parse, remove file relocation; test.py local acct detection by @-sign 2026-08-08 22:24:43 -06:00

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/.jpeg emojis 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 / --full DROP the target database. Point --target-dsn only at a database you can afford to lose.


Step-by-step migration guide

Step 0 — Requirements

  • Python ≥ 3.11 (tomllib is used to read the Mitra config) with the asyncpg package (pip install asyncpg). If your Mitra config is YAML instead of TOML, also pip 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: mitra in $PATH).
  • Read access to the Pleroma database; write access to the Mitra database.
  • Disk space in Mitra's storage_dir for your local uploads + emoji files.
  • See helper menu with python3 pleroma2mitra.py -h or 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_url must be the same domain your Pleroma instance used (https://cum.salon in our case). Local posts, media URLs and webfinger addresses are migrated with that domain baked in.
  • --target-dsn must match database_url exactly.

Step 5 — Locate Pleroma's emoji and uploads directories

  • Emoji dir (--emoji-dir): the directory containing the emoji packs — <static_dir>/emoji in a default install, usually /var/lib/pleroma/instance/static/emoji. The URL https://<domain>/emoji/<path> maps directly to <emoji-dir>/<path>.
  • Uploads dir (--uploads-dir): Pleroma's local uploader directory, uploads inside 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/).

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:

  1. Stop any running mitra processes (so any running server doesn't lose its database or hold the HTTP port).
  2. DROP and recreate the target database. You are asked to type the database name to confirm (--assume-yes skips the prompt — use it in scripts).
  3. Run Mitra's schema migrations (mitra instance-report, which applies pending migrations on startup).
  4. 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

  1. Check the row counts printed at the end of the migration against your expectations (post, actor_profile, user_account, media_attachment, emoji, ...).

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

  3. Spot-check timelines, a profile with avatar/banner, a post with an attachment, and an emoji.

  4. 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_keys and updated_at backdated 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_dir at its dist/. When building, set VITE_BACKEND_URL correctly (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_dump of 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 equal database_url in the config).
  • --config-file — Mitra config (TOML or YAML); used for storage_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-media also skips the --uploads-dir copy; --skip-posts also 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-yes to 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 (default mitra from $PATH).

The local domain is auto-detected from the source database (first local user's AP ID host).