Moving off Redmine is a data problem before it is a software problem. The issue list is the easy part to copy. The history is what people need: who changed the status, why a date slipped, what the client said in the third comment, which wiki revision was the one everybody agreed on. Specivo's migration tool is built to carry that history across, with names attached, and to tell you plainly what did not make it.
Who this is for
Teams of any size that run Redmine on their own servers and want to keep that model. Specivo is a self-hosted tracker for project tracking, a knowledge base and automation. You run it on your own infrastructure, your data stays there, and you choose how AI is used: a cloud provider, a local model, or no AI at all on an air-gapped network.
Teams usually leave for one of a few reasons: a plugin they depend on is holding back upgrades, a feature they need now sits behind a paid fork, or they want AI agents to read and write structured issue data without bolting on an extra service. Whatever the reason, the migration should not start by throwing history away.
What comes across
The export reads Redmine's database directly, in one consistent snapshot, and writes an import bundle. Specivo loads that bundle under the same identifiers, so your projects, numbers and links line up after the move.
- Projects and subprojects, with their parent projects and enabled modules.
- Trackers, statuses and priorities, plus time tracking activities and roles, including each role's user and time entry visibility.
- Users and groups, with each user's default email address, status and language, and group members.
- Memberships with their roles. Only direct grants are carried; access that comes through a group is worked out from the group again.
- Issues with full history. Comments, private notes and field changes come across in order, with the original author and timestamp, and the names of people and records shown.
- Comments and private notes as part of that history. Private issues stay private.
- Attachments and inline images, copied from Redmine's files directory and stored once per distinct file.
- Relations and parent tasks, including relations to issues in another project or in a bundle exported separately.
- Time entries, with their activity and user.
- Versions and issue categories.
- Custom fields with their values, their choices, and the trackers and projects they apply to. Each field keeps its Redmine name, and a user or version field shows the person or version by name, to readers who may see them.
- Watchers on issues and wiki pages.
- Wiki pages with every revision, each revision's author, page attachments, and the redirects Redmine left behind when a page was renamed.
- Estimates on issues.
- What non-members could read in public projects, as a separate file in the bundle.
Text is converted from Textile to Markdown. A reference to another issue, #123, becomes a link to that issue's new key when the issue is in the bundle or in an earlier one.
What does not come across, and how you see it
Some Redmine features have no equivalent in Specivo, and some data cannot be mapped exactly. Nothing is dropped silently. The bundle's manifest lists everything that was left out, and the import report shows it again when you load. Examples include:
- modules such as repositories and forums;
- per-project priorities and time tracking activities;
- custom fields on anything other than issues;
- attachments on versions, documents and forum messages;
- relation types that Specivo does not have;
- attachments whose file is missing from Redmine's files directory;
- time entries without a date.
A few behaviours are worth knowing before you run the export:
- A reference to an issue that is loaded later stays as plain
#Ntext until that issue is loaded. Load projects so that the issues they refer to come first, or load the rest later and the reference is linked on that load. - When a description was edited in Redmine, the old and new text recorded in the history stay in Redmine's Textile format.
- A value that names something Specivo does not have, such as a version that was deleted in Redmine, shows as no value in the history, except for a parent, which is kept as
#123. - Projects scheduled for deletion in Redmine are exported as active projects.
How the move works
- Export. Run the tool next to your Redmine database with a database account that can only read. It writes a directory (or a
.zip) with one file per kind of record and the attachments. - Validate. The bundle is checked for schema errors, broken references, missing or altered files and wrong counts, without writing anything to Specivo.
- Dry run. Specivo loads the whole bundle inside a transaction that is always rolled back, and reports the project keys it will use, collisions and everything it will skip, and why.
- Load. Run the same bundle for real. A load that is interrupted can be resumed.
- Check. Compare counts and a few issues in Specivo with Redmine, then cut over.
Install the tool from its release tag, on a machine that can reach Redmine's database and files directory:
uv tool install git+https://github.com/specivo/redmine-migration-tool@v0.1.0
Then export, validate and load:
# 1. Export (on a host that can reach Redmine's database and files directory)
export REDMINE_DB_URL='postgresql://redmine_reader:[email protected]/redmine'
specivo-redmine-migration \
--files-dir /var/lib/redmine/files \
--instance tracker.example.com \
--out redmine-export \
--validate
# 2 to 4. On the Specivo side, using the bundle directory
python -m specivo.cli.import_data --source bundle --path redmine-export --validate
python -m specivo.cli.import_data --source bundle --path redmine-export --dry-run
python -m specivo.cli.import_data --source bundle --path redmine-export
Give the export an --instance name that identifies your Redmine installation. Specivo uses it to remember which record became which, so a later export of the same installation can be loaded and only the new records are added.
Moving one project at a time
Large installations, or teams that want to move in stages, can export one project at a time with --project. Load the bundles in order. Every load after the first uses --continue-import, and a shared links file (--links-file) keeps track of references that were waiting for a project not loaded yet, so they resolve when that project arrives. Give every load of the same source the same links file.
Supported versions and databases
The tool is tested against Redmine 7.0.1 on PostgreSQL 16 and MySQL 8.4. It needs Python 3.12 or later and reads either database directly, so you do not need Redmine's REST API enabled.
Requirements and safety
- Your servers only. The export and the load both run on infrastructure you control. The tool writes the bundle to your disk and sends nothing anywhere. You move the files yourself, over a channel you choose.
- Read-only access to Redmine. Give the tool a database account that can only
SELECT. The export runs in one read-only transaction, so it does not change Redmine. - A dry run before every load. You see the whole result, including what will be skipped, before any record is written.
- Specivo 0.11.0 or later to load the bundle. The built-in Redmine importer was removed in that release, and this tool replaces it.
A bundle with passwords in it is a secret. More on that below.
FAQ
Can we keep Redmine running during the move?
Yes. The export only reads. For a large installation, run it on a replica or at a quiet hour, because the read snapshot is held for the whole export. When you are close to cutover, run one more export with the same instance name and load it with --continue-import. Records that are new since the first load are added. Records that were already imported are skipped, not updated, so changes made to an existing issue after the first load do not overwrite it. Plan the cutover so that the final load is the one people rely on.
Can we run the import again?
Yes. Loading the same bundle again with --continue-import creates nothing that already exists. If a run stopped halfway, --resume picks it up where it stopped.
What happens to users and passwords?
By default, every imported account must set a new password at its first sign-in, and administrators come in as ordinary accounts. If you want people to keep their Redmine password, run the export with --import-passwords and the load with the same option, on a Specivo that has the matching password hasher enabled. Administrators always set a new password. Accounts that sign in through an external directory such as LDAP have no local password in Redmine, so the bundle carries none for them, and the report lists them. Keep a bundle with passwords in it as protected as you would a copy of Redmine's database: delete it once the import is done.
What about plugins?
The tool reads Redmine's core tables and its files directory. Modules that Redmine itself handles, such as repositories and forums, are listed as not carried over. Data a plugin keeps in its own tables is not read by the tool, so check the export's report and the manifest before you rely on any plugin-specific data.
Do we have to move everything at once?
No. Export per project and load the bundles in order, as described above.
Where to start
- Stand up a Specivo instance. The Docker install guide takes about ten minutes and does not touch your Redmine.
- Export a single small project with
--projectand run the validation and a dry run on it. - Read the report, fix anything you disagree with, and repeat.
- Run the full export and load when you are ready.
The migration tool is open source and lives at github.com/specivo/redmine-migration-tool, with its release notes and install instructions. For how Specivo runs once it is in place, see the Specivo documentation, and if your data has to stay on a network with no internet access, the air-gapped install guide covers that setup.
Redmine earned its years of service, and for many teams it has a few more in it. When it is time to move, the history should come with you.