Note on methodology: During a live migration that hit every issue described below, I worked with Claude (Anthropic) to diagnose problems and organize the findings into this report. The migration, the decisions, and the workarounds were mine; the structured write up was drafted with Claude's help because I wanted these findings documented in a form useful to other migrators rather than lost in a chat log. Happy to clarify any specific finding.
Summary
Used EasyBackupBundle v2.0.2 to migrate Kimai 2.52.0 → 2.60.0 across two Docker hosts. Backup creation worked well and produced a clean, portable artifact. The plugin's restore mechanism failed silently. Manual extraction of database_dump.sql and direct mysql < restore succeeded.
This issue documents the workarounds in one place for anyone hitting the same wall. The plugin is still useful as a backup creation tool; treat the restore mechanism as broken and use the bundled SQL dump manually.
The plugin is upstream-unmaintained per the README, so this is documentation for the next migrator rather than a request for fixes. If the project gets a maintainer or a fork, the items below are the concrete things to address.
Environment
- Source: Kimai 2.52.0 in Docker (
kimai/kimai2:apache), MySQL 8.x, deployed via Coolify Service template
- Target: Kimai 2.60.0 in Docker (
kimai/kimai2:apache), MySQL 8.3, deployed via Coolify Docker Compose Empty with bind mounts
- Plugin: EasyBackupBundle v2.0.2 installed in both containers at
/opt/kimai/var/plugins/EasyBackupBundle/
- PHP: 8.x (whatever ships with
kimai/kimai2:apache at the time of writing)
What worked
- ✅ Plugin produced a clean, portable backup zip containing:
manifest.json
database_dump.sql (the actual Kimai data — schema + INSERT statements)
templates/invoice/ and templates/export/ (default Kimai templates)
var/plugins/EasyBackupBundle/ (the plugin's own source code, for some reason)
.env reference (essentially empty in Docker installs since DATABASE_URL comes from container env vars, not a .env file)
- ✅ Single-file zip made it easy to transfer between hosts (scp over Tailscale in our case)
- ✅ The mysqldump it produced inside the container was valid and restorable manually
What didn't work, with workarounds
1. escapeshellarg() argument count error on PHP 8.x (Controller, line 474)
PHP 8 enforces strict argument counts. The line:
$mysqlCmd = str_replace('{database}', escapeshellarg(trim($path), '/'), $mysqlCmd);
passes 2 arguments to escapeshellarg(), which only accepts 1. The '/' was clearly intended as the trim character but ended up in the wrong function call.
Error:
escapeshellarg() expects exactly 1 argument, 2 given
at EasyBackupController.php line 474
Fix (one-line patch):
$mysqlCmd = str_replace('{database}', escapeshellarg(trim($path, '/')), $mysqlCmd);
Applied in our migration via:
docker exec <kimai-container> sed -i \
"474s|escapeshellarg(trim(\$path), '/')|escapeshellarg(trim(\$path, '/'))|" \
/opt/kimai/var/plugins/EasyBackupBundle/Controller/EasyBackupController.php
docker exec -u www-data <kimai-container> /opt/kimai/bin/console cache:clear
2. mysqldump not present in the official kimai/kimai2:apache Docker image
The plugin hardcodes /usr/bin/mysqldump. The stock Kimai Docker image does not include the mysql client tools. The error in the plugin's log is cryptic:
ERROR: sh: 1: /usr/bin/mysqldump: not found
A user with no Docker experience would have a hard time diagnosing this. The plugin could either:
- Detect missing
mysqldump at backup-page load time and show a clear warning, or
- Document the install requirement in the Readme.
Workaround:
docker exec <kimai-container> apt update && \
docker exec <kimai-container> apt install -y default-mysql-client
Caveat: this is lost when the container is rebuilt. A proper fix would be a custom Kimai image that bakes in the dependency, or an updated official image with mysql-client preinstalled (perhaps optionally via build arg).
3. Same issue with git
The plugin also runs git (to record the EasyBackupBundle's own commit hash in the manifest). Same problem: git: not found in the stock image. Same workaround:
docker exec <kimai-container> apt install -y git
This one is non-fatal (the plugin logs it and proceeds), so it's less urgent than mysqldump, but it does pollute the log with sh: 1: git: not found.
4. Strict filename regex prevents descriptive suffixes
In EasyBackupController.php:
public const REGEX_BACKUP_ZIP_NAME = '/^\d{4}-\d{2}-\d{2}_\d{6}\.zip$/';
This means a file like 2026-06-12_172458_my-old-server.zip will not appear in the plugin's UI list, even though the timestamp prefix is unambiguous. A user migrating multiple installs benefits from naming backups descriptively (which host they came from). The strict regex forces them to be renamed back to bare-timestamp form before placing in var/easy_backup/.
A relaxed regex like /^\d{4}-\d{2}-\d{2}_\d{6}(_.+)?\.zip$/ would accept descriptive suffixes while still requiring the timestamp prefix.
Workaround: rename the backup file to strict YYYY-MM-DD_HHMMSS.zip format before placing it in var/easy_backup/.
5. Restore reports SUCCESS in the log but does NOT restore the database (silent failure)
Even after applying the line 474 patch, the restore action through the UI:
- Says "success" in the flash message
- Writes a detailed log to
easybackup.log showing every step completing
- Includes a "Copying ..." line for every file in the backup
- Says "Remove temp dir" at the end
- Leaves the application database empty
Verified via Kimai's CLI:
docker exec <kimai-container> /opt/kimai/bin/console kimai:user:list
Before restore: only the temp admin we created via ADMINMAIL/ADMINPASS.
After "successful" restore: still only that temp admin. Two previous users were missing.
The plugin's restoreAction clearly attempts to run mysql with the dump file, but the actual database is unchanged. Possible root causes (we did not investigate further because the manual workaround was so straightforward):
- The constructed mysql command may have an additional bug beyond line 474 — perhaps in how the host or port is substituted, or how the password is escaped
- The
{host} substitution might use the wrong hostname (the plugin appears to assume mysql but Docker Compose service names can differ — ours was sqldb)
- Empty/missing port substitution may produce a malformed command that fails silently
- The
shell_exec or passthru call may not surface failures
Workaround that worked (manual SQL restore):
# Inside the Kimai container, extract just the SQL dump from the backup zip
docker exec <kimai-container> unzip -p \
/opt/kimai/var/easy_backup/<backup>.zip \
database_dump.sql > /tmp/kimai_dump.sql
# Copy it to the MySQL container
docker cp /tmp/kimai_dump.sql <sqldb-container>:/tmp/kimai_dump.sql
# Run the restore against the kimai database using the kimai user
docker exec <sqldb-container> sh -c \
'mysql -u<dbuser> -p"$MYSQL_PASSWORD" kimai < /tmp/kimai_dump.sql'
# If the source and target Kimai versions differ, upgrade the schema:
docker exec -u www-data <kimai-container> \
/opt/kimai/bin/console doctrine:migrations:migrate --no-interaction
# Verify users restored
docker exec <kimai-container> /opt/kimai/bin/console kimai:user:list
6. Backup zip contains the plugin's own source — restore overwrites your patched version
The backup zip includes var/plugins/EasyBackupBundle/ (the plugin code itself). When restore copies files from the zip to the target Kimai install, it overwrites the plugin's own files. If you applied the line 474 patch in the target, restore reverts it back to the buggy original version from the source backup.
For the silent-restore-failure workaround above, this didn't matter (we used manual SQL only). But anyone applying the patch and then relying on the plugin's full restore mechanism would have their patch undone.
Workaround: re-apply patches after any restore that touches the plugin directory.
A cleaner long-term fix would be to exclude the plugin's own directory from backup, or to skip it on restore.
7. easy_backup/ directory not in default Kimai Docker bind-mounts
The official Kimai docker-compose example only persists var/data/ and var/plugins/. Backups created by EasyBackup land in var/easy_backup/, which is NOT one of the persistent paths in the standard setup. Result: any backup created via the plugin is lost the next time the container is rebuilt (image update, env var change, etc.).
Workaround: add a bind mount for easy_backup/:
volumes:
- /opt/kimai/data:/opt/kimai/var/data
- /opt/kimai/plugins:/opt/kimai/var/plugins
- /opt/kimai/backups:/opt/kimai/var/easy_backup # add this
This should arguably be mentioned in the plugin's Readme.
Schema migrations across Kimai versions
Not a plugin bug per se, but worth noting for anyone doing a cross-version migration: if the source Kimai (where the backup was made) and the destination Kimai (where you're restoring) are different versions, the restored SQL dump will be the source version's schema. The destination Kimai code expects the destination version's schema.
This produces errors like:
Doctrine\DBAL\Exception\InvalidFieldNameException:
An exception occurred while executing a query:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 't0.signature_date' in 'field list'
Fix (always run after a cross-version restore):
docker exec -u www-data <kimai-container> \
/opt/kimai/bin/console doctrine:migrations:migrate --no-interaction
This is standard Doctrine practice and not the plugin's fault, but a one-line note in the Readme about "if you're restoring from an older Kimai version, run migrations after" would save time.
Recommended honest framing
For anyone reading this issue: the plugin is still worth using for backup creation. The backup zip it produces is a clean, portable artifact that genuinely solves the cross-host transfer problem. The data inside (especially database_dump.sql) is correct and usable.
The restore portion needs work. Until someone takes the plugin over (or forks it), the safest pattern is:
- Use EasyBackup to create the backup zip
- Apply the line 474 patch on the target host
- Use the plugin to display the backup in the UI (so you know it's recognized)
- Manually restore using
database_dump.sql + mysql < instead of clicking Restore in the UI
- Run
doctrine:migrations:migrate if versions differ
- Re-apply patches if you depend on them
Hope this saves someone the night we spent figuring it out.
Versions tested
- EasyBackupBundle: 2.0.2
- Kimai (source): 2.52.0
- Kimai (target): 2.60.0
- PHP: bundled with
kimai/kimai2:apache (8.x)
- MySQL: 8.x (source), 8.3 (target)
- Docker Compose v2
- Coolify v4.x as orchestration layer
References
Note on methodology: During a live migration that hit every issue described below, I worked with Claude (Anthropic) to diagnose problems and organize the findings into this report. The migration, the decisions, and the workarounds were mine; the structured write up was drafted with Claude's help because I wanted these findings documented in a form useful to other migrators rather than lost in a chat log. Happy to clarify any specific finding.
Summary
Used EasyBackupBundle v2.0.2 to migrate Kimai 2.52.0 → 2.60.0 across two Docker hosts. Backup creation worked well and produced a clean, portable artifact. The plugin's restore mechanism failed silently. Manual extraction of
database_dump.sqland directmysql <restore succeeded.This issue documents the workarounds in one place for anyone hitting the same wall. The plugin is still useful as a backup creation tool; treat the restore mechanism as broken and use the bundled SQL dump manually.
The plugin is upstream-unmaintained per the README, so this is documentation for the next migrator rather than a request for fixes. If the project gets a maintainer or a fork, the items below are the concrete things to address.
Environment
kimai/kimai2:apache), MySQL 8.x, deployed via Coolify Service templatekimai/kimai2:apache), MySQL 8.3, deployed via Coolify Docker Compose Empty with bind mounts/opt/kimai/var/plugins/EasyBackupBundle/kimai/kimai2:apacheat the time of writing)What worked
manifest.jsondatabase_dump.sql(the actual Kimai data — schema + INSERT statements)templates/invoice/andtemplates/export/(default Kimai templates)var/plugins/EasyBackupBundle/(the plugin's own source code, for some reason).envreference (essentially empty in Docker installs since DATABASE_URL comes from container env vars, not a.envfile)What didn't work, with workarounds
1.
escapeshellarg()argument count error on PHP 8.x (Controller, line 474)PHP 8 enforces strict argument counts. The line:
passes 2 arguments to
escapeshellarg(), which only accepts 1. The'/'was clearly intended as the trim character but ended up in the wrong function call.Error:
Fix (one-line patch):
Applied in our migration via:
2.
mysqldumpnot present in the officialkimai/kimai2:apacheDocker imageThe plugin hardcodes
/usr/bin/mysqldump. The stock Kimai Docker image does not include the mysql client tools. The error in the plugin's log is cryptic:A user with no Docker experience would have a hard time diagnosing this. The plugin could either:
mysqldumpat backup-page load time and show a clear warning, orWorkaround:
Caveat: this is lost when the container is rebuilt. A proper fix would be a custom Kimai image that bakes in the dependency, or an updated official image with mysql-client preinstalled (perhaps optionally via build arg).
3. Same issue with
gitThe plugin also runs
git(to record the EasyBackupBundle's own commit hash in the manifest). Same problem:git: not foundin the stock image. Same workaround:This one is non-fatal (the plugin logs it and proceeds), so it's less urgent than mysqldump, but it does pollute the log with
sh: 1: git: not found.4. Strict filename regex prevents descriptive suffixes
In
EasyBackupController.php:This means a file like
2026-06-12_172458_my-old-server.zipwill not appear in the plugin's UI list, even though the timestamp prefix is unambiguous. A user migrating multiple installs benefits from naming backups descriptively (which host they came from). The strict regex forces them to be renamed back to bare-timestamp form before placing invar/easy_backup/.A relaxed regex like
/^\d{4}-\d{2}-\d{2}_\d{6}(_.+)?\.zip$/would accept descriptive suffixes while still requiring the timestamp prefix.Workaround: rename the backup file to strict
YYYY-MM-DD_HHMMSS.zipformat before placing it invar/easy_backup/.5. Restore reports SUCCESS in the log but does NOT restore the database (silent failure)
Even after applying the line 474 patch, the restore action through the UI:
easybackup.logshowing every step completingVerified via Kimai's CLI:
Before restore: only the temp admin we created via
ADMINMAIL/ADMINPASS.After "successful" restore: still only that temp admin. Two previous users were missing.
The plugin's
restoreActionclearly attempts to run mysql with the dump file, but the actual database is unchanged. Possible root causes (we did not investigate further because the manual workaround was so straightforward):{host}substitution might use the wrong hostname (the plugin appears to assumemysqlbut Docker Compose service names can differ — ours wassqldb)shell_execorpassthrucall may not surface failuresWorkaround that worked (manual SQL restore):
6. Backup zip contains the plugin's own source — restore overwrites your patched version
The backup zip includes
var/plugins/EasyBackupBundle/(the plugin code itself). When restore copies files from the zip to the target Kimai install, it overwrites the plugin's own files. If you applied the line 474 patch in the target, restore reverts it back to the buggy original version from the source backup.For the silent-restore-failure workaround above, this didn't matter (we used manual SQL only). But anyone applying the patch and then relying on the plugin's full restore mechanism would have their patch undone.
Workaround: re-apply patches after any restore that touches the plugin directory.
A cleaner long-term fix would be to exclude the plugin's own directory from backup, or to skip it on restore.
7.
easy_backup/directory not in default Kimai Docker bind-mountsThe official Kimai docker-compose example only persists
var/data/andvar/plugins/. Backups created by EasyBackup land invar/easy_backup/, which is NOT one of the persistent paths in the standard setup. Result: any backup created via the plugin is lost the next time the container is rebuilt (image update, env var change, etc.).Workaround: add a bind mount for
easy_backup/:This should arguably be mentioned in the plugin's Readme.
Schema migrations across Kimai versions
Not a plugin bug per se, but worth noting for anyone doing a cross-version migration: if the source Kimai (where the backup was made) and the destination Kimai (where you're restoring) are different versions, the restored SQL dump will be the source version's schema. The destination Kimai code expects the destination version's schema.
This produces errors like:
Fix (always run after a cross-version restore):
This is standard Doctrine practice and not the plugin's fault, but a one-line note in the Readme about "if you're restoring from an older Kimai version, run migrations after" would save time.
Recommended honest framing
For anyone reading this issue: the plugin is still worth using for backup creation. The backup zip it produces is a clean, portable artifact that genuinely solves the cross-host transfer problem. The data inside (especially
database_dump.sql) is correct and usable.The restore portion needs work. Until someone takes the plugin over (or forks it), the safest pattern is:
database_dump.sql+mysql <instead of clicking Restore in the UIdoctrine:migrations:migrateif versions differHope this saves someone the night we spent figuring it out.
Versions tested
kimai/kimai2:apache(8.x)References