First steps: Install dependencies

Invoice Ninja on Ubuntu production servers depends on PHP 7.4 and multiple PHP 7.4 extensions that aren’t yet available on PHP 8.0, mariadb for SQL, and because it is a laravel application we will install composer. I don’t believe you need composer to install, or update Invoice Ninja on Ubuntu. You may consider removing composer, but I would suggest keeping it installed anyways, because it is generally reccomended on production servers running laravel applications.

# apt update
# apt dist-upgrade -y

# apt install php7.4 php7.4-{fpm,bcmath,ctype,fileinfo,json,mbstring,npm,pdo,tokenizer,xml,curl,zip,gmp,gd,mysqli} mariadb-server mariadb-client curl git nginx vim composer -y

Done installing things. Let’s configure them.

Debian Users

You have different paths for php-fpm’s unix socket than Ubuntu users. This guide assumes you spawn /run/php/php-fpm.sock for a socket, but you do not. Other distros based on Debian or Ubuntu should pay attention to php-fpm.sock mentions in the nginx config example below, and change them to match the php-fpm path on your distro.

Check where php-fpm runs its socket with:

 # systemctl status php7.4-fpm

Second: Configure Mariadb and PHP

Start, and enable services for mariadb, the program/service that manages your SQL database and the incoming and outgoing communication it has with other applications like Invoice Ninja.

# systemctl enable --now mariadb

This command will take you through a guided wizard to initialize the SQL database.

# mysql_secure_installation
Enter current password for root (enter for none): 

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

These commands will directly access the SQL database through the services provided by mariadb. We will create a database with any arbitrary name ‘ninjadb’ in this example, and create arbitrary username and password combination ‘ninja’ and ‘ninjapass’. The database name will be used by InvoiceNinja during the server setup after installation is complete, as well as the username and password you specify here, in order for InvoiceNinja to login to the SQL database with read/write permission.

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

You may get a timeout error if your php execution time is default 60 seconds, and this may cause an setup loop which stated below.

To mitigate this issue, edit the php.ini file

sudo vi /etc/php/7.4/fpm/php.ini

type: /max_execution_time hit enter to find the line:

max_execution_time = 60

move the cursor to the end, hit ‘i’ to insert mode and add a zero at the end.

max_execution_time = 600

 

Third: Configure NGINX

 

Secure NGINX

The default NGINX install on Ubuntu has a pesky default website located at /etc/nginx/sites-enabled/default – and for our cases, we do not want this default website hosted by nginx. When left unconfigured, this page presents some security loopholes. It can also cause conflicts sometimes. Lets remove it.

# rm /etc/nginx/sites-enabled/default

NGINX configuration page for website

Create a text file with the ‘.conf’ ending in the /etc/nginx/conf.d/ directory, and any code in it will be included and run with the settings under /etc/nginx/nginx.conf.

# vim /etc/nginx/conf.d/invoiceninja.conf

Press ‘i’ to enter insert mode, and paste this server configuration.

 

Review it line by line, and edit the server name, root path, ssl path, php-fpm socket path, etc, as necessary.

 

You cannot copy-paste this entire document, you will need to edit appropriate sections to accommodate your own environment. I will try to “”“indicate””” when specific strings of text need your attention.

  server {
# NOTE That the 'default_server' option is only necessary if this is your primary domain application.
# If you run multiple subdomains from the same host already, remove the 'default_server' option.
   listen       443 ssl http2 default_server;
   listen       [::]:443 ssl http2 default_server;
   server_name  """invoices.example.ca""";
   client_max_body_size 20M;

 # This if statement will forcefully redirect any connection attempts to explicitly use the domain name.  
 # If not, and your DNS doesn't provide IP address protection, accessing the server with direct IP can
 # cause glitches with the services provided by the app, that could be security, or usability issues.

   if ($host != $server_name) {
     return 301 https://$server_name$request_uri;
   }

 # Here, enter the path to your invoiceninja directory, in the public dir.  VERY IMPORTANT
 # DO NOT point the root directly at your invoiceninja directory, it MUST point at the public folder
 # This is for security reasons.
   root         /usr/share/nginx/"""invoiceninja"""/public;

   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;



   ssl_session_cache shared:SSL:1m;

   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 php requests to the php7.4-fpm listen socket.  
      # PHP errors are often because this value is not correct.  
      # Verify your php7.4-fpm.sock socket file exists at the below directory
      # and that the php7.4-fpm service is running.
           fastcgi_pass unix:/run/php/php7.4-fpm.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;
  }

That’s great! Let’s now secure the Nginx

Create SSL cert:

 

Before doing this, you will need to publish your server with a public IP, and configure the DNS name.

Then follow this page to install the latest certbot:https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx

Schedule the renew:

sudo crontab -e

add below:

0 0,12 * * * python -c 'import random; import time; time.sleep(random.random()*3600)'&&certbot renew

then consolidate the configuration into a single server {} for 80 and 443, and the final file looks like:

 server {
# NOTE That the 'default_server' option is only necessary if this is your primary domain application.
# If you run multiple subdomains from the same host already, remove the 'default_server' option.
   listen       443 ssl http2 default_server;
   listen       [::]:443 ssl http2 default_server;
   server_name  """invoices.example.ca""";
   client_max_body_size 20M;

 # This if statement will forcefully redirect any connection attempts to explicitly use the domain name.  
 # If not, and your DNS doesn't provide IP address protection, accessing the server with direct IP can
 # cause glitches with the services provided by the app, that could be security, or usability issues.

   if ($host != $server_name) {
     return 301 https://$server_name$request_uri;
   }

 # Here, enter the path to your invoiceninja directory, in the public dir.  VERY IMPORTANT
 # DO NOT point the root directly at your invoiceninja directory, it MUST point at the public folder
 # This is for security reasons.
   root         /usr/share/nginx/"""invoiceninja"""/public;

   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;



   ssl_session_cache shared:SSL:1m;

   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 php requests to the php7.4-fpm listen socket.  
      # PHP errors are often because this value is not correct.  
      # Verify your php7.4-fpm.sock socket file exists at the below directory
      # and that the php7.4-fpm service is running.
           fastcgi_pass unix:/run/php/php7.4-fpm.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 { add_header Strict-Transport-Security max-age=2592000; # root /usr/share/nginx/invoiceninja/public; # rewrite ^ https://$server_name$request_uri? permanent; # listen 443 ssl default_server; # managed by Certbot ssl_certificate /etc/letsencrypt/live/"""invoices.example.ca"""/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/i"""invoices.example.ca"""/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } server { if ($host = """invoices.example.ca""") { return 301 https://$host$request_uri; } # managed by Certbot listen 80 default_server; root /usr/share/nginx/invoiceninja/public; server_name """invoices.example.ca"""; return 404; # managed by Certbot

Next Steps.

For Ubuntu 20.04, I had to disable apache2 in order to enable nginx to run on ports 80 and 443 without conflict. You might prefer to use apache2, but I am only supporting one web server conf file, and am already using nginx.

Disable & stop Apache2. Start & enable NGINX.

 $ sudo systemctl stop apache2
  $ sudo systemctl disable apache2
  $ sudo systemctl start nginx
  $ sudo systemctl enable nginx

 

Fourth: Installing Invoice Ninja

 

Installing the application files in the NGINX web server directory.

Please visit https://github.com/invoiceninja/invoiceninja/releases 328 to get the latest github release of InvoiceNinja from the team. Look closely at what you are downloading, the list also includes recent updates to v4.

  $ cd /usr/share/nginx
  $ sudo mkdir invoiceninja && cd invoiceninja
  $ sudo wget <latest zip url>
  $ sudo unzip invoiceninja.zip

Populate .env file with new random encryption key

You have an automatically generated .env file with preset encryption key and ready to initialize the setup page.

Strongly reccomend you to, populate the .env file with a new genuine encryption key. Then BACKUP this file, because it is your key to your invoicing data. Do not lose this forever by accidentally deleting or overwriting it one day.

  # php7.4 artisan key:generate

Prepare the environment after installation / any changes

Often when you are hit with a 500 or page not working problem, you just have to run one of these two commands to verify permissions and reoptimize the software.

Set permissions for the directory and all its contents to allow web server permission to view and edit files.

  # chown -R www-data:www-data /usr/share/nginx/invoiceninja

Run auto configure process, something you must do again if you ever change the values of .env or other files within the invoiceninja directory.

  # php7.4 artisan optimize

Enable Cron job automation

Now, there is need to enable cron job that will run some sort of regular maintenance, or you get a nasty red exclamation mark error in InvoiceNinja after logging in. See here for more: https://invoiceninja.github.io/selfhost.html#installing-invoice-ninja 109

  $ sudo -u www-data crontab -e

Then copy paste the following into the bottom of your cron file, which will be run for something to do with laravel which InvoiceNinja depends on.

This will run, as user www-data, with the explicitly correct php version, the artisan schedule command in order to provide Invoice Ninja application with the backend services it needs to work properly. Edit the path according to your environment

  * * * * * php7.4 /usr/share/nginx/"""invoiceninja"""/artisan schedule:run >> /dev/null 2>&1

PDF preview and generator

 

Use SNAPPDF

 

We will use

sudo apt install ca-certificates fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils -y
cd /usr/share/nginx/invoiceninja/

sudo ./vendor/bin/snappdf download

sudo vi .env
find below line PDF_GENERATOR:

PDF_GENERATOR=snappdf

#PHANTOMJS_KEY='a-demo-key-with-low-quota-per-ip-address'
#PHANTOMJS_SECRET=

Remember to also run artisan optimize after making changes to your .env file:

 # php artisan optimize

 

Now open a browser, and type the url of the invoiceninja, and start the database, email configuration.

 

 

Problems

 

Setup loop

The setup timed out at the first time, retried after changed the timeout on php. got setup loop

check the log: storage/logs/laravel.log

production.INFO: SQLSTATE[42S01]: Base table or view already exists: 10
50 Table 'languages' already exists

User below to clear the halfway migration:

php artisan migrate:reset

 

Customize the invoice template

 

Most of the tutorial can be find here:

https://invoiceninja.github.io/docs/custom-fields/

Some tips:

in the Multi line text area: if you want to enter a paragraph, you will need to type it in the format of:

 

&lt;p&gt; &lt;/p&gt;