Redmine backup and restore: the complete guide

30 July 2026 · by Jobin Joseph

A Redmine backup has three parts, and most guides only cover one of them. If you back up the database and nothing else, your restore will come up with every issue intact and every attachment missing. This guide covers what to back up, how to automate it, and — the part almost nobody writes about — how to verify that the backup you have would actually restore.

What a complete Redmine backup contains

1. The database. Every issue, project, user, journal entry, custom field value and workflow rule. This is the part everyone remembers.

2. The files directory. Attachments live on disk, not in the database. By default this is files/ under the Redmine root — check attachments_storage_path in config/configuration.yml if it has been moved. A database-only backup restores an issue that references an attachment that no longer exists.

3. Configuration and customization. config/database.yml, config/configuration.yml, your installed plugins under plugins/, and any themes under public/themes/. Plus, if you have them, custom patches to Redmine core — which you will not remember at 2 a.m. eighteen months from now.

A backup missing any one of these is not a backup. It is a partial export that will fail at the worst possible moment.

Backing up the database

MySQL / MariaDB

mysqldump -u redmine -p --single-transaction --routines --triggers \
  redmine_production | gzip > redmine_db_$(date +%F).sql.gz

--single-transaction takes a consistent snapshot without locking tables, so the instance stays available during the dump. Without it, a backup taken during active use can capture a half-written transaction.

PostgreSQL

pg_dump -U redmine -Fc redmine_production > redmine_db_$(date +%F).dump

The custom format (-Fc) is compressed and lets you restore selectively with pg_restore.

Backing up files

tar -czf redmine_files_$(date +%F).tar.gz \
  -C /path/to/redmine files plugins public/themes config/configuration.yml

Attachments accumulate faster than anyone expects. On an instance a few years old, files/ is usually far larger than the database — which matters for how long a restore takes and how much you are paying to store snapshots.

Automating it

A backup you run by hand is a backup that stops happening. Put both commands in a script, run it from cron, and ship the output somewhere that is not the same server:

#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR=/var/backups/redmine
DATE=$(date +%F)
mkdir -p "$BACKUP_DIR"

mysqldump -u redmine -p"$DB_PASS" --single-transaction --routines --triggers \
  redmine_production | gzip > "$BACKUP_DIR/db_$DATE.sql.gz"

tar -czf "$BACKUP_DIR/files_$DATE.tar.gz" \
  -C /opt/redmine files plugins public/themes config/configuration.yml

aws s3 sync "$BACKUP_DIR" s3://your-bucket/redmine/ --storage-class STANDARD_IA
find "$BACKUP_DIR" -type f -mtime +14 -delete

set -euo pipefail is the important line. Without it the script continues past a failed dump and cheerfully uploads a zero-byte file over your last good backup.

Off-server storage is not optional. A backup on the same disk as the thing it backs up protects you against exactly one failure mode — someone deleting a row — and none of the others.

Restoring

Database

# MySQL / MariaDB
gunzip < redmine_db_2026-08-14.sql.gz | mysql -u redmine -p redmine_production

# PostgreSQL
pg_restore -U redmine -d redmine_production --clean redmine_db_2026-08-14.dump

Files

tar -xzf redmine_files_2026-08-14.tar.gz -C /path/to/redmine

Then, and this is the step people skip

cd /path/to/redmine
bundle install
RAILS_ENV=production bundle exec rake db:migrate
RAILS_ENV=production bundle exec rake redmine:plugins:migrate
RAILS_ENV=production bundle exec rake tmp:cache:clear

If you are restoring onto a different Redmine version than the backup came from, db:migrate is what reconciles the schema. If you are restoring plugins, redmine:plugins:migrate is what makes them work. Skipping either produces an instance that boots and then throws 500s on the first page that touches the affected tables.

The part nobody does: verify the restore

An unverified backup is a hypothesis. Once a quarter, restore to a throwaway environment and check:

Write down how long the restore took. That number is your actual recovery time objective, and it is almost always longer than anyone assumed.

Common failure modes

The backup succeeded but the disk was full. mysqldump writes a truncated file and returns a non-zero exit code that nothing is checking. Monitor backup file size, not just whether the job ran.

The attachments path was moved and nobody updated the script. Check attachments_storage_path after any migration or upgrade.

Plugin schema drift. Plugins add their own tables. Restoring a database that contains plugin tables into an installation that no longer has those plugins leaves orphaned data and, sometimes, a broken migration state.

Encoding mismatch. Restoring a utf8mb4 dump into a utf8mb3 database silently mangles non-Latin characters and emoji. Match the collation.

The restore was never tested. By far the most common one.

Or don't run any of this

On RedminePRO, backups are continuous and encrypted, with point-in-time recovery, and disaster-recovery procedures are validated as part of business continuity. There is no cron script to maintain and no quarterly restore drill to forget. If you want your data back at any point — including if you decide to leave — we provide the full SQL dump and files on request, so you can self-host again whenever you choose.

See how managed Redmine hosting works →

Related reading: what Redmine actually costs to run yourself, and moving a self-hosted Redmine to managed hosting.

Jobin Joseph

Jobin Joseph is the founder of RedminePRO, HAZERCLOUD's fully-managed Redmine hosting service on AWS. He works on Redmine migration, operations and hosting day to day.