Skip to content

Plugin works partially on PHP 8.x + Kimai 2.x in Docker — backup useful, restore needs manual workaround #76

Description

@esodmumixam

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:

  1. Use EasyBackup to create the backup zip
  2. Apply the line 474 patch on the target host
  3. Use the plugin to display the backup in the UI (so you know it's recognized)
  4. Manually restore using database_dump.sql + mysql < instead of clicking Restore in the UI
  5. Run doctrine:migrations:migrate if versions differ
  6. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions