How to Install Dolibarr 23 on Debian 13 with Nginx and PHP 8.4
Dolibarr is one of the few open source ERP and CRM platforms that stays genuinely lightweight while still covering the core needs of a small business: quoting, invoicing, CRM, projects and accounting. Where Odoo or ERPNext ask for a Python stack, message queues and several gigabytes of RAM before they even boot, Dolibarr runs comfortably as a classic PHP application in a single LAMP, or in our case LEMP, container.
This guide walks through a clean Dolibarr 23.0.3 deployment on Debian 13, using Nginx instead of Apache, PHP 8.4-fpm, and MariaDB. The setup runs inside an Incus container, but the same steps apply to any Debian 13 VPS or bare metal host.
Why Dolibarr, why Nginx, why this stack
Dolibarr has been actively developed since 2003, carries a GPL v3 licence and has well over a million installations worldwide. It upgrades cleanly across major versions, which matters a lot once you are responsible for a production instance over several years.
Most Dolibarr tutorials default to Apache, mainly because Dolibarr ships .htaccess files for it out of the box. Those files are simply ignored under Nginx, which is not a problem, just a detail to be aware of: any access restrictions Apache would enforce through .htaccess need to be defined explicitly in the Nginx server block instead. If your existing infrastructure already runs Nginx as a reverse proxy, as is the case in most modern container-based setups, there is no reason to introduce Apache as a second web server just for one application.
Debian 13 ships PHP 8.4 by default and Dolibarr 23.x officially supports PHP versions from 7.1 through 8.4. That means no third-party PHP repository (such as Sury) is needed, no version pinning and no future conflicts when the distribution’s PHP version moves forward. Fewer moving parts, less to maintain.
Comparison: Dolibarr stack choices
| Component | Choice in this guide | Alternative | Why |
|---|---|---|---|
| Web server | Nginx | Apache + mod_php | Matches modern reverse-proxy setups, no .htaccess dependency |
| PHP handler | PHP 8.4-fpm | PHP 8.2/8.3-fpm | Debian 13 default, officially supported by Dolibarr 23.x |
| Database | MariaDB | PostgreSQL | Most common in the Dolibarr community, simplest defaults |
| Isolation | Incus container | Docker, bare VPS | Lightweight, no container runtime overhead, easy snapshotting |
Prerequisites
A Debian 13 host or container with root access, a domain name pointed at the server (or reverse proxy), and basic comfort with the command line. If you are running this behind an existing Nginx reverse proxy with SSL termination, as I do, the container itself only needs to serve plain HTTP internally.
Step 1: Update the system and install the stack
apt update && apt upgrade -y
apt install -y nginx mariadb-server \
php8.4-fpm php8.4-mysql php8.4-gd php8.4-curl php8.4-intl \
php8.4-mbstring php8.4-xml php8.4-zip php8.4-soap \
php8.4-ldap php8.4-bcmath unzip curlOne note: php8.4-imap is no longer packaged in Debian 13’s default repositories. It is only needed if you plan to use Dolibarr’s email collector module to fetch messages via IMAP. Skip it for a standard install; it can be added later through PECL if needed.
Verify the install:
php -v
systemctl status nginx php8.4-fpm mariadbYou should see PHP 8.4.x reported and all three services active and running.
Step 2: Secure MariaDB and create the database
Run the hardening script:
mariadb-secure-installationWhen prompted, skip setting a root password — MariaDB on Debian uses Unix socket authentication by default, meaning the OS root user already has implicit DBA access simply by being logged in as root. Adding a password here is unnecessary and just creates something to manage and potentially lose. For the remaining prompts: remove anonymous users, disallow remote root login, remove the test database, reload privileges — all yes.
Generate a random password for the Dolibarr database user and create the database in a single step:
DOLI_DB_PASS=$(openssl rand -base64 32 | tr -d '/+=') && echo "DB Password: $DOLI_DB_PASS"
mariadb <<EOF
CREATE DATABASE dolibarr CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'dolibarr'@'localhost' IDENTIFIED BY '$DOLI_DB_PASS';
GRANT ALL PRIVILEGES ON dolibarr.* TO 'dolibarr'@'localhost';
FLUSH PRIVILEGES;
EOFBecause we are connecting through the local Unix socket as root, there is no need for -u root or a password prompt here. Save the generated password somewhere safe; you will need it in Step 4.
Step 3: Download Dolibarr and set up the webroot
cd /tmp
curl -LO https://github.com/Dolibarr/dolibarr/archive/refs/tags/23.0.3.tar.gz
tar xzf 23.0.3.tar.gz
mv dolibarr-23.0.3/htdocs /var/www/dolibarr
chown -R www-data:www-data /var/www/dolibarrDolibarr stores all uploaded files, generated invoices and PDFs in a separate “documents” directory. This should always live outside the webroot, so it can never be served directly over HTTP:
mkdir -p /var/www/dolibarr-documents
chown -R www-data:www-data /var/www/dolibarr-documents
chmod 750 /var/www/dolibarr-documentsStep 4: Configure Dolibarr
Copy the example configuration and lock down its permissions:
cp /var/www/dolibarr/conf/conf.php.example /var/www/dolibarr/conf/conf.php
chown www-data:www-data /var/www/dolibarr/conf/conf.php
chmod 640 /var/www/dolibarr/conf/conf.phpPopulate it with the database credentials and paths. Set your domain in a variable first, so it only needs to be typed once:
DOLI_DOMAIN="your-domain.example"Replace your-domain.example with your actual domain before running this.
cat > /var/www/dolibarr/conf/conf.php << EOF
<?php
\$dolibarr_main_url_root='https://$DOLI_DOMAIN';
\$dolibarr_main_document_root='/var/www/dolibarr';
\$dolibarr_main_url_root_alt='/custom';
\$dolibarr_main_document_root_alt='/var/www/dolibarr/custom';
\$dolibarr_main_data_root='/var/www/dolibarr-documents';
\$dolibarr_main_db_host='localhost';
\$dolibarr_main_db_port='3306';
\$dolibarr_main_db_name='dolibarr';
\$dolibarr_main_db_prefix='llx_';
\$dolibarr_main_db_user='dolibarr';
\$dolibarr_main_db_pass='$DOLI_DB_PASS';
\$dolibarr_main_db_type='mysqli';
\$dolibarr_main_db_character_set='utf8mb4';
\$dolibarr_main_db_collation='utf8mb4_unicode_ci';
\$dolibarr_main_authentication='dolibarr';
EOF
chown www-data:www-data /var/www/dolibarr/conf/conf.php
chmod 640 /var/www/dolibarr/conf/conf.phpDouble-check the password and domain landed correctly:
grep -E 'db_pass|url_root' /var/www/dolibarr/conf/conf.phpStep 5: Configure Nginx
Create the server block. Note the heredoc is unquoted (<< EOF) so that $DOLI_DOMAIN gets expanded by bash, while Nginx’s own variables, like $uri and $fastcgi_script_name, are escaped with a backslash so bash leaves them untouched:
cat > /etc/nginx/sites-available/dolibarr << EOF
server {
listen 80;
server_name $DOLI_DOMAIN;
root /var/www/dolibarr;
index index.php;
location ^~ /install/ {
deny all;
}
location / {
try_files \$uri \$uri/ =404;
}
location ~ \.php\$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}
EOF
ln -s /etc/nginx/sites-available/dolibarr /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginxThe /install/ location is left open for now; we lock it down completely in the final step, after the web installer has run.
If you are running a separate reverse proxy with SSL termination (Nginx, on a different host or container), point it at this container’s internal IP address over plain HTTP, and forward the standard proxy headers:
location / {
proxy_pass http://10.0.x.x;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}Step 6: Run the web installer
Open https://your-domain.example/install/ in a browser. The installer checks PHP requirements, tests the database connection using the credentials from conf.php, creates all tables, and lets you set the initial admin account.
You may see a file integrity warning about a missing promise.js file under the CKEditor vendor directory. This is a known, harmless gap in the official release tarball; it has no effect on functionality and can be safely ignored.
Step 7: Lock the installer
This is the step most guides skip, and it matters. Once installation completes, lock it immediately:
touch /var/www/dolibarr-documents/install.lock
chown www-data:www-data /var/www/dolibarr-documents/install.lockThen update the Nginx block to actively block the install directory, rather than just leaving it unlinked:
cat > /etc/nginx/sites-available/dolibarr << EOF
server {
listen 80;
server_name $DOLI_DOMAIN;
root /var/www/dolibarr;
index index.php;
location ^~ /install/ {
deny all;
}
location / {
try_files \$uri \$uri/ =404;
}
location ~ \.php\$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}
EOF
nginx -t && systemctl reload nginxVerify with a browser: https://your-domain.example/install/ should now return a 403 Forbidden, while the main login page loads normally.
Troubleshooting: reopening /install/ later
If you ever need to revisit the installer, for example to repair an install or because the deny all; rule above got applied before the installer was actually run, simply flipping deny all; to allow all; is not enough. The ^~ prefix modifier on location ^~ /install/ makes Nginx commit to that block for any URL under /install/, which means the regex-based location ~ \.php$ block elsewhere in the file never gets a chance to run for that path. The practical symptom is that the browser downloads index.php as a raw file instead of rendering the installer; the request curl -I head check confirms it, returning Content-Type: application/octet-stream instead of text/html.
The fix is to give the /install/ block its own nested PHP handling rather than relying on allow all; alone:
cat > /etc/nginx/sites-available/dolibarr << EOF
server {
listen 80;
server_name $DOLI_DOMAIN;
root /var/www/dolibarr;
index index.php;
location ^~ /install/ {
try_files \$uri \$uri/ =404;
location ~ \.php\$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
include fastcgi_params;
}
}
location / {
try_files \$uri \$uri/ =404;
}
location ~ \.php\$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}
EOF
nginx -t && systemctl reload nginxOnce the installer step is complete again, revert this immediately back to the flat deny all; version from Step 7, which doesn’t need the nested PHP block at all, since a deny rule applies regardless of file type. Leaving /install/ open with PHP execution enabled, even briefly, is a real exposure window; close it as soon as the installer has done its job.
What’s next
At this point you have a working, secured Dolibarr instance: PHP 8.4, MariaDB, Nginx, documents stored outside the webroot and the installer locked down. The next layer is module selection, which is where Dolibarr’s modular design actually pays off; you activate only what the business needs (CRM, quoting, invoicing, projects, event organisation) instead of inheriting a bloated default setup. That will be a follow-up article.
FAQ
Yes. Dolibarr 23.x officially supports PHP from version 7.1 through 8.4, which lines up exactly with Debian 13’s default PHP version, so no third-party PHP repository is required.
Yes, without any limitation. Dolibarr ships .htaccess files intended for Apache, but these are simply not used under Nginx. Any access restriction they provide, such as blocking the install directory, needs to be replicated explicitly in the Nginx server block, as shown above.
This is a known gap in the official 23.0.3 GitHub release tarball, related to a CKEditor vendor file. It does not affect Dolibarr’s functionality and can be safely dismissed.
Always outside the public webroot. Dolibarr stores generated invoices, uploaded attachments and other sensitive files there, and if it sits inside a path the web server serves directly, those files could become accessible over HTTP.
Yes, it works fine and is in fact the most common setup in the Dolibarr community. The choice here for Nginx is purely about consistency with an existing Nginx-based infrastructure, not a functional requirement.
This happens when the location ^~ /install/ block only contains an allow all; or similar rule without its own PHP handling. The ^~ prefix modifier stops Nginx from falling through to the regex-based .php location elsewhere in the config, so PHP files under /install/ get served as static files instead of being executed. The fix is to nest a PHP location block inside /install/ itself, as shown in the troubleshooting section above.
