Installing InvoiceNinja v5 Beta on Enterprise Linux 8 (RHEL, AlmaLinux, Rocky Linux)

I strongly reccomend installing on Ubuntu based distro, and NOT RHEL based for compatibility

I will start by saying I reccomend installing on Ubuntu. On rpm-based distros you cannot automatically update the package, and must do it manually, increasing the risk of user error and data loss, and increasing demand on manual labour. You must backup your .env file, and the contents of the public/storage directory, to preserve your data before updating. Updating must be done manually by downloading the source code, and installing with composer and node, which are unnecessary for you on Ubuntu also. This guide will be preserved mostly for reference, educational purposes, freedom of choice, etc. I initially tried hosting on CentOS 8 before any other distro, which led me into all of this.

This guide is also going to be updated and supported more slowly now than the Ubuntu guide, so expect the Ubuntu guide to be more up to date, and effectively written.

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 invoices;
MariaDB .. > create user 'ninja'@'localhost' identified by 'ninjapass';
MariaDB .. > grant all privileges on invoices.* 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 chown -R nginx:nginx /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.

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/

Often the php/session directory changes ownership and breaks functions, like when php restarts. This ACL permission change will guaruntee persistent functionality. These policies set default permissions for these folders with nginx as 'another' owner of the directory and any new contents of it.

 $ sudo setfacl -dm u:nginx:rwx /var/lib/php/session
 $ sudo setfacl -dm u:nginx:rwx /var/lib/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

Fedora, and RedHat based users will need to install source code to run the program, because npm pulls different dependencies for some systems. So use git to download the latest code, and pull the v5-stable branch for the best current release available.

$ sudo git clone https://github.com/invoiceninja/invoiceninja
$ sudo rsync -av invoiceninja/ ./
$ sudo rm -rf invoiceninja
$ git checkout v5-stable

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

Generate your .env file.

$ sudo cp .env.example .env

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 https://getcomposer.org/installer | 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 -u nginx php -d memory_limit=-1 `which composer` i --no-dev

Optionally

increase PHP memory limit yourself, and no longer forced to workaround memory limmit issue, get better PHP performance too, as long as you have good amount of RAM to use. Use your text editor of choice.

$ sudo vim /etc/php.ini

You will find and edit the line beginning with memory_limit = and change the following value to 1024M. In vim, remember you can find fast with the '/' key in command mode.

memory_limit = 1024M

From then on you can run the following instead of this especially long elaborate php command above:

 $ sudo -u nginx php composer i --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 chown -R nginx:nginx ./
$ sudo -u nginx npm install --no-optional

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

$ sudo -u nginx php artisan key:generate

And then auto-configure the server. Run php artisan optimize again anytime you edit the files or make changes in the invoiceninja installation directory.

$ sudo -u nginx 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: 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 ;)

$ 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 web console, 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.

Backing up InvoiceNinja critical files

Two things need to be backed up, if you don't just backup your entire nginx directory often. 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. Preferably to a mountpoint on another physical storage device, so that you have something to rebuild with if you lose this one suddenly.

I will not cover it here since there's many resources for it already, but I suggest to setup cron jobs to perform this DB and env backup on a regular basis.

For "ROOTPASS" you can skip this variable and it will prompt for password, but to automate this, you need to pass the password in plain text. You could probably use any other user for this with the permissions set for the whole db though, to avoid passing rootsql pass in plaintext to a cron file.

  $ cp .env /mnt/yourbackupdisk/backups-invoiceninja/invoice-backup-envfile
  $ sudo mysqldump -u root -p ROOTPASS invoice-db > /mnt/yourbackupdisk/backups-invoiceninja/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 Enterprise Linux 8 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 Enterprise Linux 8.