Install Invoice Ninja v5 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 http://rpms.remirepo.net/enterprise/remi-release-8.rpm
$ 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 https://rpm.nodesource.com/setup_14.x | sudo -E bash
$ sudo yum install nodejs -y

verify that npm and node are installed correctly.

$ node -v 
v14.7.0

$ npm -v 
6.14.7

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;
    server_name  invoices.example.ca;
    # 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;
    server_name invoices.example.ca;
    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

OPTIONAL FOR TESTING purposes or individual use 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.

127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4 invoices.example.ca
192.168.0.88  invoices2.example.ca

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
listen.group = nginx
listen.mode = 0660
env[HOSTNAME] = $HOSTNAME
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

the ‘actual’ process, begin by navigating to the ‘installation directory’ for invoiceninja.

$ cd /usr/share/nginx/invoiceninja

As of this writing, we are on beta release v5.0.13. Visit https://github.com/invoiceninja/invoiceninja/releases for the latest release, and download the SOURCE CODE .zip package only when following this tutorial.

$ sudo wget https://github.com/invoiceninja/invoiceninja/archive/v5.0.13-release.zip
$ sudo unzip v5.0.13-release.zip

move the files, and hidden files, up into the current directory where we expect them to be.

$ sudo mv invoiceninja-5.0.13-release/* .
$ sudo mv invoiceninja-5.0.13-release/.* .

Alternatively install from github:

Updating this with these instructions because often the updates with bug fixes are out fast on the github pull, but the releases are at least stable and reliable also. Depending on the day, you could get a ‘better’ build from github.

# cd /usr/share/nginx/invoiceninja
# git clone https://github.com/invoiceninja/invoiceninja .

Install composer

$ curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/bin --filename=composer

Seems necessary to workaround php memory limit issues on most systems. This runs ‘composer update’ when ‘composer update’ fails with an out of memory error. This process may take a while.

$ sudo php -d memory_limit=-1 `which composer` update

npm update will download and install your dependencies, including chromium for PDF support. But npm may give some warnings for vulnerability concerns. We will address those as well.

$ sudo npm update --unsafe-perm=true 

Expect you will have 2 priority vulnerability warnings at this point. Run npm audit fix to fix one of them.

$ sudo npm audit fix --unsafe-perm=true

Now I expect you will have 1 low priority vulnerability remaining, requiring manual fixing. To confirm which dependency, and which version is needed for it, run;

$ sudo npm audit

Assuming everything so far is right, you will see ‘yargs-parser’ requesting version >=18.1.2. Now we begin the manual fix.

$ sudo vim package.json

at the bottom of the file you will see the following section:

{
    ….
    "dependencies": {
        "@tailwindcss/ui": "^0.1.3",
        "axios": "^0.19",
        "card-js": "^1.0.13",
        "card-validator": "^6.2.0",
        "cross-env": "^7.0",
        "jsignature": "^2.1.3",
        "laravel-mix": "^5.0.1",
        "lodash": "^4.17.13",
        "puppeteer": "^1.20.0",
        "resolve-url-loader": "^3.1.0",
        "sass": "^1.15.2",
        "sass-loader": "^8.0.0",
        "tailwindcss": "^1.4"
    }
}

Don’t remove anything, but add the following section for ‘resolutions’ after the ‘dependencies’ section. Make sure to note the exact use of the comma and squigly brackets for correct syntax.

{
    (beginning of file)
    …
    “dependencies”: {
        …
        "tailwindcss": "^1.4"
},
    "resolutions": {
        "yargs-parser": "^18.1.2"
    }
}

Copy example/stock .env file over to prepare invoice ninja for clean setup.

$ sudo cp .env.example .env
$ sudo chown -R nginx:nginx ./

Run NPM commands to update and install dependencies

Run command to force resolutions to satisfy security dependencies, and use --no-optional silence warnings about unneeded MacOS dependencies. Should have no errors or vulnerabilities if run with all these options.

$ sudo npx npm-force-resolutions && npm i --no-optional --unsafe-perm=true

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

$ sudo php artisan key:generate

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 php artisan optimize
$ sudo php artisan storage:link

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: https://wiki.gentoo.org/wiki/SELinux/Tutorials/Permissive_versus_enforcing

$ sudo vim /etc/selinux/config
...
SELINUX=disabled

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 https://127.0.0.1:9090 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.

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

3 Likes