
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.
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.
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.
pg_dump -U redmine -Fc redmine_production > redmine_db_$(date +%F).dumpThe custom format (-Fc) is compressed and lets you restore selectively with pg_restore.
tar -czf redmine_files_$(date +%F).tar.gz \
-C /path/to/redmine files plugins public/themes config/configuration.ymlAttachments 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.
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 -deleteset -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.
# 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.dumptar -xzf redmine_files_2026-08-14.tar.gz -C /path/to/redminecd /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:clearIf 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.
An unverified backup is a hypothesis. Once a quarter, restore to a throwaway environment and check:
SELECT COUNT(*) FROM issues; on both.log/production.log on first boot.Write down how long the restore took. That number is your actual recovery time objective, and it is almost always longer than anyone assumed.
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.
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.
Related reading: what Redmine actually costs to run yourself, and moving a self-hosted Redmine to managed hosting.