Upgrading Dolibarr from the command line

Upgrade Dolibarr without the web wizard: upgrade.php, upgrade2.php and step5.php, one step per version, the upgrade.unlock trap that blocks step 2 without any error, and a complete script with backup and rollback, tested from 22 to 24.

Versions concerned
Dolibarr 22.0.5, 23.0.4 and 24.0.1 upgraded to 24.0.2 (official .tgz archives) · Debian 13, PHP 8.4, MariaDB 11.8
What was checked
Tests on 19/10/2026 on Debian 13 (PHP 8.4.26, MariaDB 11.8.6) with the official archives from SourceForge: fresh installs of 22.0.5, 23.0.4 and 24.0.1 with the web wizard, with a third party, an invoice, an attached file, an API key and a folder in custom/; the commands on this page run under a sudo account; the script run from all three versions; migration in a single call, second step blocked by upgrade.unlock, scripts run from another folder and under another account; forced failure (ALTER privilege removed) to test the rollback; structure of each database compared column by column with a fresh 24.0.2; REST API called before and after. Dolibarr 24.0.2 code read (install/upgrade.php, upgrade2.php, step5.php, inc.php, check.php).

The Dolibarr web wizard (/install) upgrades in a few clicks. But if you manage your server over SSH, run several instances, or the page times out before the end, you will want to run the same thing from the command line. Dolibarr allows it: these are the three scripts the wizard calls, upgrade.php, upgrade2.php and step5.php.

The commands are short. The traps cannot be seen: these scripts answer “all good” (exit code 0) even when they did nothing. We triggered three of them on 19 October 2026, then wrote a script that avoids them, tested by upgrading 22.0.5, 23.0.4 and 24.0.1 to 24.0.2.

This page is for an installation made from the .tgz archive, as in our guide Install Dolibarr on Ubuntu or Debian: the code in /var/www/dolibarr, the documents in /var/lib/dolibarr/documents. With the official Docker image, read Upgrading Dolibarr in Docker instead.

Before you start

The upgrade itself rarely breaks Dolibarr: external modules, the PHP version and customisations are what cause trouble (see Upgrading Dolibarr without breaking anything). Our free module Upgrade readiness check runs that check beforehand, without changing anything.

Then write down the version of your database, not of the files: it decides the steps.

sudo mariadb -N dolibarr -e "SELECT name, value FROM llx_const WHERE name LIKE 'MAIN_VERSION_LAST_%' AND entity = 0"

The highest value shown is your current version. Replace dolibarr with the name of your database, and llx_ with your prefix if it differs (both are in conf.php).

1. Back up

Do it even if you rely on your host’s backup: the rollback below needs it.

sudo mariadb-dump --single-transaction --routines dolibarr > ~/dolibarr-base.sql
sudo tar -czf ~/dolibarr-documents.tgz -C /var/lib/dolibarr documents

The old Dolibarr files are kept in the next step. More details: Back up and restore Dolibarr.

2. Replace the files

Put the new version in a fresh folder rather than copying it over the old one: files removed between two versions then really disappear, and the old folder is your way back. Then copy two things back: conf.php and the custom folder of external modules.

cd /tmp
curl -fL -o dolibarr-24.0.2.tgz "https://sourceforge.net/projects/dolibarr/files/Dolibarr%20ERP-CRM/24.0.2/dolibarr-24.0.2.tgz/download"
sudo tar --no-same-owner -xzf /tmp/dolibarr-24.0.2.tgz -C /var/www
sudo mv /var/www/dolibarr /var/www/dolibarr-old
sudo mv /var/www/dolibarr-24.0.2 /var/www/dolibarr
sudo cp -a /var/www/dolibarr-old/htdocs/conf/conf.php /var/www/dolibarr/htdocs/conf/
sudo cp -a /var/www/dolibarr-old/htdocs/custom/. /var/www/dolibarr/htdocs/custom/

cp -a keeps the owner and permissions of conf.php (in our test, www-data, read only).

3. Migrate the database: one step per version

Each step goes from one major version to the next, with the three scripts in order. The numbers are written x.0.0: Dolibarr ignores the last digit of your version (the 5 in 22.0.5) and picks the same file either way.

From 22 to 24, two steps:

sudo touch /var/lib/dolibarr/documents/upgrade.unlock
cd /var/www/dolibarr/htdocs/install
sudo -u www-data php upgrade.php 22.0.0 23.0.0 > /tmp/step1.html
sudo -u www-data php upgrade2.php 22.0.0 23.0.0 >> /tmp/step1.html
sudo -u www-data php step5.php 22.0.0 23.0.0 auto upgrade_23.0.0 >> /tmp/step1.html
sudo -u www-data php upgrade.php 23.0.0 24.0.0 > /tmp/step2.html
sudo -u www-data php upgrade2.php 23.0.0 24.0.0 >> /tmp/step2.html
sudo -u www-data php step5.php 23.0.0 24.0.0 >> /tmp/step2.html

From a 23.x or a 24.0.x, a single step (it is also the one for a minor upgrade, 24.0.1 to 24.0.2):

sudo touch /var/lib/dolibarr/documents/upgrade.unlock
cd /var/www/dolibarr/htdocs/install
sudo -u www-data php upgrade.php 23.0.0 24.0.0 > /tmp/step1.html
sudo -u www-data php upgrade2.php 23.0.0 24.0.0 >> /tmp/step1.html
sudo -u www-data php step5.php 23.0.0 24.0.0 >> /tmp/step1.html

For a bigger gap, add one step per version on the same pattern: every step except the last one ends with auto upgrade_ followed by the version it reaches.

4. Check, then lock again

grep -o '[0-9.]*-[0-9.]*\.sql' /tmp/step*.html | sort -u
sudo mariadb -N dolibarr -e "SELECT value FROM llx_const WHERE name = 'MAIN_VERSION_LAST_UPGRADE' AND entity = 0"

The first command must show one file per step (step1.html:22.0.0-23.0.0.sql then step2.html:23.0.0-24.0.0.sql; with a single step, just 23.0.0-24.0.0.sql): it is the only reliable sign that the migration ran. The second must show the new version. Then put back the protections from the installation guide:

sudo rm -f /var/lib/dolibarr/documents/upgrade.unlock
sudo find /var/www/dolibarr/htdocs -path /var/www/dolibarr/htdocs/custom -prune -o -exec chmod a-w {} +
sudo chmod 400 /var/www/dolibarr/htdocs/conf/conf.php

Log in and check in SetupModules/Applications that your external modules are still enabled. Keep /var/www/dolibarr-old for a few days before deleting it.

The traps we triggered

What you do What happens Exit code
Run upgrade.php while install.lock exists, without upgrade.unlock One sentence, “The install/upgrade pages have been disabled…”, the database is not touched 0
Chain two steps without auto upgrade_23.0.0 at the end of the first step5.php deletes upgrade.unlock after step 1: step 2 is blocked as above 0
Go from 22 to 24 in a single call Both SQL files run, but the three data migrations of 23 are skipped 0
Run the script from another folder Failed opening required '../filefunc.inc.php' 255
Run the script under an account that cannot read conf.php Permission denied on conf.php 255

The second one is the most dangerous. In our test, the database already showed “24.0.2” after step 1 although step 2 had not run: it lacked 3 tables and 37 columns of version 24, and the REST API answered 401 with a key that worked before. The web wizard avoids the trap by passing upgrade_23.0.0 to intermediate steps (it is written in install/check.php): the database is then marked 23.0.0, and upgrade.unlock stays in place until the last step.

The third one does not show right away. With a single 2224 call, the final structure was identical to the two-step one, but the data migrations specific to 23 did not run (in our database, the API key was not copied into its new table, and the signing key of the unalterable logs was not created). Nothing visibly broke in 24.0.2: that is exactly the problem. With three versions or more between them, it is worse: according to the code of upgrade.php, only the first and the last SQL files are picked, which our Docker test from 20 to 24 showed (incomplete database, without any error).

The last two cases stop with a visible error: work from htdocs/install and run the scripts with the web server account (www-data on Debian and Ubuntu).

The complete script

The script below does all of the above, in order, and stops at the first doubt. It reads the database settings from conf.php, works out the steps, backs up, replaces the files, migrates step by step, checks after each step that the SQL file did run and that the database carries the right version, then locks again. If something fails, it puts the old files back and reloads the database backup.

Copy it into a file named maj-dolibarr.sh, adapt the five lines at the top, then run it:

sudo bash maj-dolibarr.sh
#!/bin/bash
# Upgrade Dolibarr from the command line (installation from the .tgz archive).
# Guide and explanations: https://dolix.fr/en/notes/dolibarr-script-mise-a-jour/
# Tested on 19/10/2026 on Debian 13 (PHP 8.4, MariaDB 11.8): 22.0.5, 23.0.4 and 24.0.1 -> 24.0.2.
# Run as root (sudo bash maj-dolibarr.sh), outside working hours.
set -euo pipefail

# --- Adapt ------------------------------------------------------------------
NOUVELLE=24.0.2                     # version to install
DOLI=/var/www/dolibarr              # folder that contains htdocs/
DOCS=/var/lib/dolibarr/documents    # documents folder ($dolibarr_main_data_root)
SAUV=/root/sauvegardes-dolibarr     # where to store the backups
WEB=www-data                        # web server user
# -----------------------------------------------------------------------------

CONF=$DOLI/htdocs/conf/conf.php
DATE=$(date +%Y%m%d-%H%M%S)
TRAVAIL=$SAUV/$DATE
[ "$(id -u)" -eq 0 ] || { echo "Run as root (sudo)."; exit 1; }
[ -f "$CONF" ] || { echo "conf.php not found: $CONF"; exit 1; }
mkdir -p "$TRAVAIL"; chmod 700 "$SAUV" "$TRAVAIL"

# Database settings, read from conf.php
lire() { php -r 'include $argv[1]; echo ${$argv[2]}??"";' "$CONF" "$1"; }
DB_HOTE=$(lire dolibarr_main_db_host); DB_PORT=$(lire dolibarr_main_db_port)
DB_NOM=$(lire dolibarr_main_db_name); DB_USER=$(lire dolibarr_main_db_user)
DB_MDP=$(lire dolibarr_main_db_pass); PREFIXE=$(lire dolibarr_main_db_prefix)
case "$DB_MDP" in crypted:*|dolcrypt:*)
  echo "Encrypted password in conf.php: this script cannot read it."; exit 1;; esac
MYCNF=$(mktemp); chmod 600 "$MYCNF"; trap 'rm -f "$MYCNF"' EXIT
printf '[client]\nhost=%s\nport=%s\nuser=%s\npassword="%s"\n' \
  "$DB_HOTE" "${DB_PORT:-3306}" "$DB_USER" "$DB_MDP" > "$MYCNF"
SQL() { mariadb --defaults-extra-file="$MYCNF" -N "$DB_NOM" -e "$1"; }

# Version of the database (not of the files)
ACTUELLE=$(SQL "SELECT value FROM ${PREFIXE:-llx_}const WHERE name IN
  ('MAIN_VERSION_LAST_UPGRADE','MAIN_VERSION_LAST_INSTALL') AND entity=0" | sort -V | tail -1)
[ -n "$ACTUELLE" ] || { echo "Database version not found."; exit 1; }
M=${ACTUELLE%%.*}; C=${NOUVELLE%%.*}
[ "$M" -le "$C" ] || { echo "The database ($ACTUELLE) is newer than $NOUVELLE."; exit 1; }

# Steps: one per major version (22 -> 23, then 23 -> 24), never 22 -> 24 in one go
ETAPES=()
if [ "$M" -eq "$C" ]; then ETAPES=("$((C - 1)).0.0 $C.0.0")
else for ((v = M; v < C; v++)); do ETAPES+=("$v.0.0 $((v + 1)).0.0"); done; fi
echo "Database at $ACTUELLE, target $NOUVELLE. Steps:"; printf '  %s\n' "${ETAPES[@]}"
read -r -p "Backup, then upgrade. Continue? (y/N) " R; [ "$R" = y ] || exit 1

# 1. Download and extract the new version (nothing is changed yet)
curl -fsSL -o "$TRAVAIL/dolibarr-$NOUVELLE.tgz" \
  "https://sourceforge.net/projects/dolibarr/files/Dolibarr%20ERP-CRM/$NOUVELLE/dolibarr-$NOUVELLE.tgz/download"
tar --no-same-owner -xzf "$TRAVAIL/dolibarr-$NOUVELLE.tgz" -C "$TRAVAIL"
[ -f "$TRAVAIL/dolibarr-$NOUVELLE/htdocs/install/upgrade.php" ] || { echo "Incomplete archive."; exit 1; }

# 2. Backups: database, documents (the old files are kept in step 3)
mariadb-dump --defaults-extra-file="$MYCNF" --single-transaction --routines --skip-disable-keys "$DB_NOM" > "$TRAVAIL/base.sql"
tar -czf "$TRAVAIL/documents.tgz" -C "$(dirname "$DOCS")" "$(basename "$DOCS")"
echo "Backups in $TRAVAIL"

# 3. Replace the files: new folder, then conf.php and external modules
ANCIEN=$DOLI-old-$DATE
mv "$DOLI" "$ANCIEN"
mv "$TRAVAIL/dolibarr-$NOUVELLE" "$DOLI"
cp -a "$ANCIEN/htdocs/conf/conf.php" "$DOLI/htdocs/conf/conf.php"
cp -a "$ANCIEN/htdocs/custom/." "$DOLI/htdocs/custom/"
chown -R "$WEB:$WEB" "$DOLI/htdocs/custom"

retour() {
  set +e
  echo "FAILED at step $1: rolling back to version $ACTUELLE."
  rm -rf "$DOLI"; mv "$ANCIEN" "$DOLI"; rm -f "$DOCS/upgrade.unlock"
  echo "Old files put back."
  # Empty the database (tables created by the migration must go too), then reload the backup
  { echo "SET FOREIGN_KEY_CHECKS=0;"
    SQL "SELECT CONCAT('DROP TABLE \`', table_name, '\`;') FROM information_schema.tables
         WHERE table_schema = DATABASE()"; } | mariadb --defaults-extra-file="$MYCNF" "$DB_NOM" \
  && mariadb --defaults-extra-file="$MYCNF" "$DB_NOM" < "$TRAVAIL/base.sql" \
  && echo "Database restored." \
  || echo "THE DATABASE COULD NOT BE RESTORED: reload $TRAVAIL/base.sql by hand."
  echo "Migration logs: $TRAVAIL/*.html"; exit 1
}

# 4. Migrate the database, step by step, as the web server user
cd "$DOLI/htdocs/install"
for E in "${ETAPES[@]}"; do
  set -- $E
  # step5.php deletes upgrade.unlock when the database reaches the files' version: recreate it at each step
  touch "$DOCS/upgrade.unlock"
  if [ "$2" = "$C.0.0" ]; then FIN=""; ATTENDU=$NOUVELLE     # last step
  else FIN="auto upgrade_$2"; ATTENDU=$2; fi                  # intermediate step, like the web wizard
  for P in upgrade.php upgrade2.php step5.php; do
    A=""; [ "$P" = step5.php ] && A=$FIN
    runuser -u "$WEB" -- php "$P" "$1" "$2" $A > "$TRAVAIL/$1-$2-$P.html" 2>&1 || retour "$P $1 -> $2"
  done
  # Exit codes are 0 even when install.lock blocks everything: check the result
  grep -q "$1-$2.sql" "$TRAVAIL/$1-$2-upgrade.php.html" || retour "$1 -> $2 (SQL script not run)"
  V=$(SQL "SELECT value FROM ${PREFIXE:-llx_}const WHERE name='MAIN_VERSION_LAST_UPGRADE' AND entity=0")
  [ "$V" = "$ATTENDU" ] || retour "$1 -> $2 (database at $V instead of $ATTENDU)"
  echo "Step $1 -> $2: OK (database at $V)"
done
rm -f "$DOCS/upgrade.unlock"

# 5. Lock again, as after the installation
[ -f "$DOCS/install.lock" ] || touch "$DOCS/install.lock"
find "$DOLI/htdocs" -path "$DOLI/htdocs/custom" -prune -o -exec chmod a-w {} +
chmod 400 "$DOLI/htdocs/conf/conf.php"
echo "Done. Old files: $ANCIEN (delete them once checked)."

What it does not do: it does not check external modules, it cannot read an encrypted password in conf.php (it stops), and it uses the mariadb and mariadb-dump commands (on a MySQL server they are called mysql and mysqldump: not tested).

What we checked with this script

  • 22.0.524.0.2 (two steps), 23.0.424.0.2 and 24.0.124.0.2: database version at 24.0.2, third party, invoice, attached file and custom/ folder present, API key accepted, /install pages blocked again, upgrade.unlock removed, conf.php still read only.
  • Structure compared column by column with a freshly installed 24.0.2: identical, except one column for databases coming from 22 and 23 (retained_warranty_amount in llx_facture, whose addition is commented out in the official 24 script and which no PHP file of 24.0.2 uses).
  • Forced failure (ALTER privilege removed from the database user): the script stopped at the first step, put the old files back and reloaded the database; structure and content identical to before, API back on 22.0.5.
  • Duration on our test database: 37 seconds, download included. A real database will take longer: run it outside working hours.

Rolling back by hand

If you followed the steps by hand and something is wrong:

sudo rm -rf /var/www/dolibarr
sudo mv /var/www/dolibarr-old /var/www/dolibarr
sudo mariadb -e "DROP DATABASE dolibarr; CREATE DATABASE dolibarr CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
sudo mariadb dolibarr < ~/dolibarr-base.sql

Check beforehand that your database is in utf8mb4 (it is if you followed our installation guide).

If the migration gets stuck

Read the /tmp/step*.html files (or those in the backup folder for the script): each refused query appears there with its error message. If you cannot find it, the €90 diagnosis is about looking at your instance with you for an hour, then giving you in writing the cause and what fixing it would cost.

See also

Dolibarrupgradescriptcommand lineCLIupgrade.phpupgrade2.phpstep5.phpupgrade.unlockinstall.lockSSH