Install Invoice Ninja v5 on CentOS 8

Installing InvoiceNinja v5 Beta on CentOS 8

Install PHP7.4, and MariaDB server.

Update OS, and enable repo for latest version of PHP 7.4

$ sudo yum update
$ sudo yum install yum-utils
$ sudo yum module reset php
$ sudo yum module enable php:remi-7.4

Dependencies for invoiceninja, npm, etc.

$ sudo yum install gcc-c++ make php php-{fpm,bcmath,ctype,fileinfo,json,mbstring,pdo,tokenizer,xml,curl,zip,gmp,gd,mysqli} mariadb-server -y

Dependencies for chromium, which is used by npm/puppeteer for rendering PDF.

$ sudo yum install libXcomposite libXcursor libXdamage libXext libXi libXtst libmng libXScrnSaver libXrandr libXv alsa-lib cairo pango atk at-spi2-atk gtk3

Start, enable, and configure mariadb/mysql

$ sudo systemctl start mariadb
$ sudo systemctl enable mariadb
$ mysql_secure_installation

Set the password for the root user of the SQL database. Make sure to keep record of this and do not lose it. You will need it in the next step, and for future maintenance of DB

Remove anonymous users? [Y/n] y
Disallow root login remotely? [Y/n] y
Remove test database and access to it? [Y/n] y
Reload privilege tables now? [Y/n] y

Create and configure the SQL database we will be using later with InvoiceNinja.

$ mysql -u root -p
Enter Password:  ******
MariaDB .. > create database db-ninja-01;
MariaDB .. > create user 'ninja'@'localhost' identified by 'ninjapass';
MariaDB .. > grant all privileges on db-ninja-01.* to 'ninja'@'localhost';
MariaDB .. > flush privileges;

Install nodejs/npm, to support Email, and PDF with puppeteer/chromium.

$ curl -sL | sudo -E bash
$ sudo yum install nodejs -y

verify that npm and node are installed correctly.

$ node -v 

$ npm -v 

Optionally configure SSL with OpenSSL, in lieu of an existing letsencrypt or other cert

I will not be giving instructions on other SSL certification methods. You can find and change the appopriate lines in the NGINX config step after this, if you plan to use another cert of your own.

Create a directory to store your ssl for nginx to access

$ sudo mkdir -p /etc/nginx/cert/

Generate SSL certificate, and follow the prompts to configure it appropriately.

$ sudo openssl req -new -x509 -days 365 -nodes -out /etc/nginx/cert/ninja.crt -keyout /etc/nginx/cert/ninja.key
$ sudo chmod 600 /etc/nginx/cert/*

Install and configure nginx

$ sudo yum install nginx
$ sudo vim /etc/nginx/conf.d/invoiceninja.conf

Settings for a TLS enabled server.

You may specify your own SSL certificate in this file if you are not using openssl above.
You will also specify your own domain name below, as per your DNS records or etc.

server {
    listen       443 ssl http2 default_server;
    listen       [::]:443 ssl http2 default_server;
    # Here, enter the path to your invoiceninja directory, in the public dir.
    root         /usr/share/nginx/invoiceninja/public;
    client_max_body_size 20M;

    gzip on;
    gzip_types application/javascript application/x-javascript text/javascript text/plain application/xml application/json;
    gzip_proxied    no-cache no-store private expired auth;
    gzip_min_length 1000;

    index index.php index.html index.htm;

    # Enter the path to your existing ssl certificate file, and certificate private key file
    # If you don’t have one yet, you can configure one with openssl in the next step.
    ssl_certificate "/etc/nginx/cert/ninja.crt";
    ssl_certificate_key "/etc/nginx/cert/ninja.key";
    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout  10m;
    ssl_ciphers 'AES128+EECDH:AES128+EDH:!aNULL';
    ssl_prefer_server_ciphers on;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    charset utf-8;

    # Load configuration files for the default server block.
    include /etc/nginx/default.d/*.conf;

    location / {
            try_files $uri $uri/ /index.php?$query_string;

    if (!-e $request_filename) {
            rewrite ^(.+)$ /index.php?q= last;

    location ~ \.php$ {
            fastcgi_split_path_info ^(.+\.php)(/.+)$;
            # Here we pass to php-fpm listen socket.  For configuration see /etc/php-fpm.d/*.conf.
            fastcgi_pass unix:/var/run/php-fpm/www.sock;
            fastcgi_index index.php;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_intercept_errors off;
            fastcgi_buffer_size 16k;
            fastcgi_buffers 4 16k;

    location ~ /\.ht {
        deny all;

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt { access_log off; log_not_found off; }

    access_log /var/log/nginx/ininja.access.log;
    error_log /var/log/nginx/ininja.error.log;

    sendfile off;


server {
    listen      80;
    add_header Strict-Transport-Security max-age=2592000;
    rewrite ^ https://$server_name$request_uri? permanent;

Create the invoiceninja directory we will be installing to later, and start and enable NGINX

$ sudo mkdir -p /usr/share/nginx/invoiceninja
$ sudo systemctl start nginx
$ sudo systemctl enable nginx

Optionally, for testing or maintenance etc;

This is only for testing before deployment situations, or for setting up pointers to local instances of InvoiceNinjav4 that you want to migrate your data from (Both instances v4 and v5 must be running on separate environments with resolvable domain names to successfully migrate data from within the InvoiceNinja v4 web UI, on the latest patched versions of v4 only).

modify hosts file to point at server IP with given domain name in nginx, if you do not yet have DNS pointed at the server, or do not yet want to point your DNS at it.

$ sudo vi /etc/hosts

and simply add your domain to the end of this list for localhost, as seen here, or add the remote IP, followed by a space, and the domain name to point at it, all on a new line, like seen below again. NGINX should reroute any http request on the domain (not the direct IP or localhost) name to https.   localhost localhost.localdomain localhost4 localhost4.localdomain4

Configure firewalld

Below steps will open ports 80 and 443 to the public on firewalld permanently.

$ sudo firewall-cmd -zone=public -add-service=http --permanent
$ sudo firewall-cmd -zone=public -add-service=https --permanent
$ sudo firewall-cmd -reload

Now you need to properly configure php-fpm.

$ sudo vim /etc/php-fpm.d/www.conf

And change each of the following lines by either editing the values, or uncommenting the lines:

user = nginx
group = nginx
listen = /var/run/php/php-fpm.sock
listen.owner = nginx = nginx
listen.mode = 0660
env[PATH] = /usr/local/bin:/usr/bin:/bin
env[TMP] = /tmp
env[TMPDIR] = /tmp
env[TEMP] = /tmp

Next we need to create a directory for the php session, and enable the php session to run at startup.

$ sudo mkdir -p /var/lib/php/session
$ sudo mkdir -p /var/run/php/
$ sudo chown -R nginx:nginx /var/lib/php/session/
$ sudo chown -R nginx:nginx /var/run/php/

Start, and enable php-fpm. We won’t need to configure it here, but be mindful of it as it is a strong dependency of InvoiceNinja.

$ sudo systemctl start php-fpm
$ sudo systemctl enable php-fpm

Installing InvoiceNinja v5!

$ cd /usr/share/nginx/invoiceninja

CentOS users, I have figured out the easiest and most consistent installation and update method for you. When you install or update you will always go from github, to the latest release, and download the Source code (zip) and extracting its contents into the installation directory. The other releases are only working on Ubuntu based distros. And the latest sourcecode from git is not necessarily stable. If you have an existing installation, you can simply extract the contents of the update and copy them ontop of the existing installation, and running at least php artisan, and sometimes composer and npm update commands below again.

$ sudo wget
$ sudo unzip -d .

Sometimes your files will extract into a folder anyways and you need a way to copy them all out with little hassle; rsync, with -av to show progress and :

$ sudo rsync -av ./invoiceninja-5.0.20/ .

First time install only, setup .env file and db encryption key:

Generate your .env file.

$ sudo cp .env.example .env

Now we a assign a randomly generated application encryption key to the software.

$ sudo php artisan key:generate

Back this up!!

Remember, this key you generated just now is an encryption key for the contents of the database entries. You need this to access any of the data on the SQL database. The artisan key:generate command populates the .env file with your encryption key. after you run this and finish setting up your db and logging into the web client, you really should backup the working .env file somewhere secure, in case you ever clobber it.

Install composer

If you don’t have Composer installed on your server yet, you will install it now, so you can download dependencies for PHP stuff with it.

$ curl -sS | sudo php -- --install-dir=/usr/bin --filename=composer

Running Composer to install InvoiceNinja Dependencies

As pointed out by another user, we can install the same versions of software that the developers intended by running composer option ‘install’ instead of ‘update’. ‘Update’ option would update the software dependencies to new versions (not standardized versions) and update the .lock file included in the package as well. The ‘–no-dev’ option stops composer from installing dependencies for the developers only.

$ sudo php -d memory_limit=-1 `which composer` install --no-dev

Run npm commands to install dependencies, especially for headless chrome, necessary for PDF generation

Running npm install as opposed to npm update again has the same intended purpose of standardization of dependencies. The ‘–no-optional’ argument will instruct npm to ignore dependencies for other OS and you won’t see the errors when those cannot install.

$ sudo npm install --no-optional

And then auto-configure the server. Run this command again anytime you edit the files or make changes in the invoiceninja installation directory. Storage:link creates symlinks that the client portal depends on to show PDFs. Run it if you ever reinstall, or PDF in client portal are broken.

$ sudo chown -R nginx:nginx ./
$ sudo php artisan storage:link
$ sudo php artisan optimize

SELINUX CHOICES: Ongoing maintenance, or disable completely - READ CAREFULLY

I’ve summarized the best I know how, to configure SELINUX appropriately for all the features in the current release of invoiceninja v5, but I might not be able to update this or support you in the future if the codebase changes a bit, or you uncover new behaviours that demand new permissions from SELinux. If you do not want to support SELinux, you can permanently set it to permissive mode or disabled. This is a personal choice, and I cannot reccomend either or for you.

IF you do not want to be bothered with SELINUX at all, disable it permanently.

Change the following line from ‘enforcing’ to or ‘disabled’ to permanently change SELINUX state. The Gentoo wiki has a nice page for more reading about the difference between these states:

$ sudo vim /etc/selinux/config

The above command typically only takes affect after reboot.

Permissive Mode

Under permissive mode, some context rules will still be applied, but those rules can be pre-emptively allowed with the commands below. First though, we must enable SELinux permissive mode, in order to effectively complete the InvoiceNinja setup, especially for PDF rendering and Chrome/Puppeteer, which requires special permissions that I am not smart enough to allow in advance yet.
To temporarily set SELINUX to permissive mode, until the next reboot, run the following:

$ sudo setenforce 0

Initial SELINUX permission setup - These commands will mostly allow you to run invoiceninja, make sure the path in quotes is accurate for your environment:

$ sudo yum install policycoreutils-python-utils

$ sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja(/.*)?'
$ sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja/public(/.*)?'
$ sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja/storage(/.*)?'
$ sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja/app(/.*)?'
$ sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja/bootstrap(/.*)?'
$ sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja/config(/.*)?'
$ sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja/database(/.*)?'
$ sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja/resources(/.*)?'
$ sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja/vendor(/.*)?'
$ sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja/tests(/.*)?'
$ sudo restorecon -Rv '/usr/share/nginx/invoiceninja/'

For you lazy people with ctrl + c fingers :wink:

$ sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja(/.*)?'; sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja/public(/.*)?'; sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja/storage(/.*)?'; sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja/app(/.*)?'; sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja/bootstrap(/.*)?'; sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja/config(/.*)?'; sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja/database(/.*)?'; sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja/resources(/.*)?'; sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja/vendor(/.*)?'; sudo semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/nginx/invoiceninja/tests(/.*)?'; sudo restorecon -Rv '/usr/share/nginx/invoiceninja/'

Some more known SELINUX policies required during the setup phase, EVEN while in SELINUX permissive mode:

$ sudo setsebool -P httpd_unified 1
$ sudo setsebool -P httpd_execmem 1
$ sudo setsebool -P httpd_can_network_connect 1

And for users in a VM, you will have an additional policy to set I believe, for example, while testing myself, I use VMware Workstation Player on Windows, and must set the policy:

$ setsebool -P use_virtualbox 1

Managing SELINUX with cockpit

I cannot provide a full instruction set on explicitly and cleanly setting SELINUX permissions with InvoiceNinja. Depending on your environment, (VM, bare metal, etc), you may have different daemons requesting different permissions also. What I can suggest, is enabling cockpit on CentOS, and using the SELINUX tab to manage SELINUX permission requests with. The most effective method, is to set SELINUX to permissive mode in cockpit before attempting invoiceninja setup on web page and after testing PDF, email, and logging in after completing setup, address any SELINUX conflicts reported in cockpit, and then re-enable SELINUX enforcing mode. Also note, this will probably be an ongoing thing to monitor, so as you test features in the new InvoiceNinja, you should enable permissive mode on SELINUX again when troubleshooting them, and monitor for SELINUX conflicts. Log into cockpit at by default, with your username and password from the system.

$ sudo systemctl enable --now cockpit.socket

For example:

When running setup the first time, you will fail PDF test with SELINUX in enforcing, \until you attempt PDF test, and run the following, then run PDF test and fail again, and then run following commands a second time in a row - a lot easier to manage if you set SELINUX to ‘permissive’ and run any conflict resolutions from cockpit after.

$ sudo ausearch -c 'chrome' --raw | audit2allow -M my-chrome
$ semodule -X 300 -i my-chrome.pp

Unfortunately, beyond these steps with SELINUX I cannot offer a more proficient command list. I am still running mostly in enforcing mode, switching back to permissive as I note a broken feature and actively monitor my SELINUX conflicts while testing features and performing work. If someone finds some modifications to the instructions that might accomodate SELINUX better, and pre-emptively configure permissions and policies for chromium and etc, then let me know somehow, I would appreciate that.

Backing up InvoiceNinja

Two things need to be backed up. You need a backup of your .env file, from the installation directory. This file has a complicated encryption key hash saved, and if this key is lost, you will lose access to your database basically, with no workaround. The entries are encrypted with this key. The other thing we can learn to backup breifly is the database as well. Both can be done with one line commands, that you must modify for your environment.

  $ cp .env /backupdir/invoice-backup-envfile
  $ sudo mysqldump -u root -p invoice-db > /backupdir/invoice-backup-db.sql

sqldump will dump the contents of the database out for you, but the contents themselves are still encrypted and you need the .env file backed up with your key as well.

Especially Important… .env

to backup your .env file, I can’t stress enough, as you host on CentOS at this time the only way to update is by copying source code package of the next update on top of the existing installation. You don’t wan’t to do something silly on accident and wipe that .env file and destroy your access to your company data.

Congratulations. You should now successfully be able to run InvoiceNinja v5 on CentOS 8.


A complementary Ubuntu 20.04 LTS version would also be fantastic.

This should work just fine. I tested it all on an Ubuntu VM tonight.

1 Like

Thanks. It’s very helpful.

Thanks for the detailed guide @TechnicallyComputers :metal:

It’s helped me perform the install on a Debian 10 (Buster) LEMP stack obviously using Debian equivalents where necessary. My Ninja setup page is redirecting back to itself on first run so I’ve got some debugging to do :hammer: :smiley:

Just a note of something I noticed while installing composer. After install you ran the update command:

$ sudo composer update

I’m new to composer but the composer documentation explains that update will install the latest available versions of all dependencies then update the lock file with the newly installed version numbers.

Now please don’t take this the wrong way, I’m merely being an inquisitive debugger in lieu of detailed documented requirements from the Invoice Ninja crew. It seems to me, the purpose of the composer.lock file is to ensure anybody deploying a project (in this case Invoice Ninja) ends up with the same suite of dependencies as when the project was successfully compiled, therefore omitting the possibility of incompatible dependencies being loaded. If the lock file is overwritten with the latest versions by the update command, we no longer have the same working environment for Ninja as when it was compiled by the Ninja devs. To quote

If there is a composer.lock file in the current directory, it will use the exact versions from there instead of resolving them. This ensures that everyone using the library will get the same versions of the dependencies.

I completely get installing the latest versions of everything by default; I’m attempting the v5 install primarily because I didn’t want to roll back my php7.4 to an older version (v4 being incompatible with 7.4). Now, you have Ninja working and so again, I’m not questioning your methods :v: :grin: but my question is I suppose, was there a reason for updating to the latest dependencies rather than abiding by the composer.lock file in the Ninja zip?

I wasn’t aware of this about composer and the lockfile to standardize dependencies. Thats very intelligent design, and I understand and support the idea for stability and standardization.

I’m going to have to research that more, and update my notes before rewriting anything yet though.

I would say this guide is for CentOS 8, and it has very different dependencies and issues than Ubuntu and probably Debian. For example, the file is not possible to install on CentOS 8. Composer may work but npm update will critically fail almost every step. CentOS 8 requires installing from exclusively. Seems the source code has this file too, so I might test that.

I’d say I only recently confirmed this to myself and didn’t update this version of the guide yet since I was making other changes as well so this wasn’t reflected when you read this guide

1 Like

I guess I wondered if you updated the dependencies beyond the .lock file versions on purpose to help with bugs. It’s my first time using any dependency managers to create ‘sandbox’ environments and I also think it’s pretty cool for standardisation.

Also, you might be interested to know that Node.js/npm has the same functionality using the .json file:

install - Installs dependencies to the dev spec in json file
update - Ignores the json file and update to latest available

There’s a short post here that explains their behaviour well if it interests you.

After I posted this I found your write up on Ubuntu :grin:

Actually both your guides helped me in different places with Debian so thank you! Now I have to fix a login screen bug :face_with_monocle:

1 Like

Login screen bug might be php-fpm related. Verify php-fpm is configured properly if you have not already. My CentOS 8 guide assumes a CentOS 8 environment, and the nginx configuration notes assume default CentOS 8 paths for php-fpm sockets. Relevant:

   fastcgi_pass unix:/var/run/php-fpm/www.sock

Configure nginx/apache to point to this socket wherever php-fpm is configured to run it on your system and ensure your web server has permissions to access it also based on the conf file for php-fpm, wherever it is installed.

I’ll make a reminder to read this stuff next weekend or something, thanks.

Thanks, PHP is ok I think, it’s pointed here in the nginx block

fastcgi_pass unix:/run/php/php7.4-fpm.sock;

I think my problem is mariadb related:

Deserializing '[data, [{permissions: , n~

It’s a server running a few other sites too so everything seems healthy

Did you try to migrate data, or clean install? Data migrations must be done through the webui on v4, running parallel to v5 with two resolvable domains, and will fail if you use SQL dump methods. If your permissions are not sticking with the defined mariadb user, you might experiment with using the root user, to try and rule out one variable. Ok good luck, I have other work. The slack chat is very active by the devs, if you need to look for a little more deeper support that’s a good resource.

It’s a clean install. Thanks for the tip that’s a good idea, I think I’ll try with root. If I solve it I’ll make a note to the solution on here as I’ve talked about it now.

Thanks for your help :+1:

EDIT: The issue causing Deserializing '[data, [{permissions: , n~ on the login modal appears to be fixed in the latest git v2 branch :partying_face:

1 Like

Thanks for the update, that’s great to hear!

1 Like

For those like me, looking for chrome headless dependencies in debian based OS: