Invoice Ninja Print

  • 0

This guide explains the native Invoice Ninja installation used by the Sive AppStore image. It is intended to help administrators understand, maintain, back up, and troubleshoot the service after deployment. The reference build installs Invoice Ninja directly on Ubuntu; it does not use Docker or another container platform.

Reference build: Invoice Ninja 5.13.26 on Ubuntu 24.04 LTS (x86_64), with Nginx, MariaDB, PHP 8.3-FPM, a systemd queue worker, and a systemd scheduler timer.

Before you begin

Run the commands as root on a fresh Ubuntu 24.04 LTS x86_64 server. Replace invoice.example.com and admin@example.com with your real values. DNS for the hostname should point to the server before requesting a TLS certificate.

The application lives in /var/www/invoiceninja. Root-only supporting secrets are kept in /etc/invoiceninja. Never paste passwords or the contents of .env into a support ticket or public terminal transcript.

1. Install the operating-system packages

apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
  ca-certificates curl fonts-liberation libasound2t64 \
  libatk-bridge2.0-0t64 libatk1.0-0t64 libcups2t64 libgbm1 \
  libgtk-3-0t64 libnspr4 libnss3 libx11-xcb1 libxcomposite1 \
  libxdamage1 libxfixes3 libxkbcommon0 libxrandr2 \
  mariadb-server nginx openssl php8.3-bcmath php8.3-cli \
  php8.3-common php8.3-curl php8.3-fpm php8.3-gd php8.3-gmp \
  php8.3-imagick php8.3-intl php8.3-mbstring php8.3-mysql \
  php8.3-soap php8.3-xml php8.3-zip qemu-guest-agent unzip xdg-utils

systemctl enable --now mariadb php8.3-fpm
systemctl enable nginx

The desktop-style libraries are runtime dependencies of the headless Chromium build used by SnapPDF to render invoices and quotations.

2. Apply the PHP settings

Create /etc/php/8.3/mods-available/invoiceninja.ini:

memory_limit=1024M
upload_max_filesize=100M
post_max_size=100M
max_execution_time=300
max_input_time=300
max_input_vars=5000
date.timezone=Africa/Johannesburg
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=30000
opcache.validate_timestamps=1
opcache.revalidate_freq=2

Enable the settings and confirm that the required extensions are present:

phpenmod invoiceninja
systemctl restart php8.3-fpm

for module in bcmath curl dom gd gmp imagick intl mbstring mysqli soap xml zip; do
  php -m | grep -Fxiq "$module" || echo "Missing PHP module: $module"
done

3. Create MariaDB and protect its password

install -d -m 0700 -o root -g root /etc/invoiceninja
DB_PASSWORD="$(openssl rand -hex 32)"
printf '%s\n' "$DB_PASSWORD" > /etc/invoiceninja/database-password
chmod 0600 /etc/invoiceninja/database-password

mysql --protocol=socket <<SQL
CREATE DATABASE IF NOT EXISTS invoiceninja
  CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS 'invoiceninja'@'localhost'
  IDENTIFIED BY '${DB_PASSWORD}';
ALTER USER 'invoiceninja'@'localhost'
  IDENTIFIED BY '${DB_PASSWORD}';
GRANT ALL PRIVILEGES ON invoiceninja.* TO 'invoiceninja'@'localhost';
FLUSH PRIVILEGES;
SQL
unset DB_PASSWORD

The generated database password contains hexadecimal characters only, which also makes it safe to use in the SQL statements above. Keep the password file readable only by root.

4. Download and verify the pinned application

The reference image uses the release and checksums below. Checksum verification is mandatory: stop if either command reports a mismatch.

VERSION="5.13.26"
WORK_DIR="$(mktemp -d /var/www/invoiceninja-install.XXXXXX)"

curl -fL --retry 4 --retry-delay 3 \
  -o "$WORK_DIR/invoiceninja.tar" \
  "https://github.com/invoiceninja/invoiceninja/releases/download/v5.13.26/invoiceninja.tar"
printf '%s  %s\n' \
  'be344fc3977394b7defda1bf636f33d42108b4045bb310e5aa91176749666dbc' \
  "$WORK_DIR/invoiceninja.tar" | sha256sum -c -

curl -fL --retry 4 --retry-delay 3 \
  -o "$WORK_DIR/ungoogled.tar" \
  'https://pdf.invoicing.co/ungoogled.tar'
printf '%s  %s\n' \
  'bc241a3a17df91dff21d57326ce5c8bf6f8ef55b8384d94e0c1007892fba2868' \
  "$WORK_DIR/ungoogled.tar" | sha256sum -c -

Extract the release and verify its internal version marker:

install -d -m 0755 "$WORK_DIR/app"
tar -xzf "$WORK_DIR/invoiceninja.tar" -C "$WORK_DIR/app"
test "$(tr -d '[:space:]' < "$WORK_DIR/app/VERSION.txt")" = "$VERSION"

systemctl stop invoiceninja-queue.service 2>/dev/null || true
systemctl stop invoiceninja-scheduler.timer 2>/dev/null || true
test ! -e /var/www/invoiceninja
mv "$WORK_DIR/app" /var/www/invoiceninja

CHROMIUM_DIR="/var/www/invoiceninja/vendor/beganovich/snappdf/versions/ungoogled"
install -d -m 0755 "$CHROMIUM_DIR"
tar -xzf "$WORK_DIR/ungoogled.tar" -C "$CHROMIUM_DIR"
printf 'ungoogled\n' > \
  /var/www/invoiceninja/vendor/beganovich/snappdf/versions/revision.txt
chmod 0755 "$CHROMIUM_DIR/chrome-linux/chrome" \
  "$CHROMIUM_DIR/chrome-linux/chrome_crashpad_handler"

If you are replacing an existing installation, do not use the test ! -e and mv sequence until you have completed the backup and upgrade procedure later in this guide.

5. Configure Invoice Ninja

Generate unique application secrets. Do not reuse the example names as actual secret values:

APP_KEY_VALUE="$(openssl rand -base64 32 | tr -d '\n')"
UPDATE_SECRET_VALUE="$(openssl rand -hex 32)"
WEBCRON_SECRET_VALUE="$(openssl rand -hex 32)"
DB_PASSWORD="$(cat /etc/invoiceninja/database-password)"

Create /var/www/invoiceninja/.env, substituting the generated shell variables and your hostname:

APP_NAME="Invoice Ninja"
APP_ENV=production
APP_KEY=base64:CHANGE_ME_TO_APP_KEY_VALUE
APP_DEBUG=false
APP_URL=https://invoice.example.com
REACT_URL=https://invoice.example.com
CALENDAR_NATIVE_REDIRECT=invoiceninja://calendar_connection/complete
REQUIRE_HTTPS=true
TRUSTED_PROXIES=

DB_CONNECTION=mysql
MULTI_DB_ENABLED=false
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=invoiceninja
DB_USERNAME=invoiceninja
DB_PASSWORD=CHANGE_ME_TO_DATABASE_PASSWORD

LOG_CHANNEL=stack
CACHE_DRIVER=file
QUEUE_CONNECTION=database
SESSION_DRIVER=file
SESSION_LIFETIME=120
INTERNAL_QUEUE_ENABLED=false

MAIL_MAILER=log
MAIL_FROM_ADDRESS=admin@example.com
MAIL_FROM_NAME="Invoice Ninja"

NINJA_ENVIRONMENT=selfhost
IS_DOCKER=false
FILESYSTEM_DRIVER=public
PDF_GENERATOR=snappdf
SNAPPDF_EXECUTABLE_PATH=/var/www/invoiceninja/vendor/beganovich/snappdf/versions/ungoogled/chrome-linux/chrome
SNAPPDF_SKIP_DOWNLOAD=true
SNAPPDF_EXECUTABLE_ARGUMENTS="--headless --disable-gpu --no-sandbox --disable-dev-shm-usage --disable-crash-reporter"

API_SECRET=
UPDATE_SECRET=CHANGE_ME_TO_UPDATE_SECRET_VALUE
WEBCRON_SECRET=CHANGE_ME_TO_WEBCRON_SECRET_VALUE
DEMO_MODE=false
SCOUT_DRIVER=null
ERROR_EMAIL=

Replace every CHANGE_ME value before continuing. Preserve the existing APP_KEY during future upgrades; changing it can make encrypted application data unreadable.

Apply ownership and permissions:

chown -R www-data:www-data /var/www/invoiceninja
find /var/www/invoiceninja -type d -exec chmod 0755 {} +
find /var/www/invoiceninja -type f -exec chmod 0644 {} +
chmod 0600 /var/www/invoiceninja/.env
chmod 0755 /var/www/invoiceninja/artisan \
  /var/www/invoiceninja/vendor/bin/snappdf \
  /var/www/invoiceninja/vendor/beganovich/snappdf/versions/ungoogled/chrome-linux/chrome \
  /var/www/invoiceninja/vendor/beganovich/snappdf/versions/ungoogled/chrome-linux/chrome_crashpad_handler

install -d -m 0775 -o www-data -g www-data \
  /var/www/invoiceninja/storage \
  /var/www/invoiceninja/storage/app \
  /var/www/invoiceninja/storage/framework/cache \
  /var/www/invoiceninja/storage/framework/sessions \
  /var/www/invoiceninja/storage/framework/views \
  /var/www/invoiceninja/storage/logs \
  /var/www/invoiceninja/bootstrap/cache

6. Configure Nginx

Create /etc/nginx/sites-available/invoiceninja:

server {
    listen 80;
    listen [::]:80;
    server_name invoice.example.com;
    root /var/www/invoiceninja/public;
    index index.php index.html;
    client_max_body_size 100M;
    server_tokens off;

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

    location = /index.php {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_read_timeout 300;
    }

    location ~ \.php$ {
        return 404;
    }

    location ~ /\.(?!well-known) {
        deny all;
    }
}

This layout forwards only /index.php to PHP-FPM and rejects requests for any other PHP file.

ln -sfn /etc/nginx/sites-available/invoiceninja \
  /etc/nginx/sites-enabled/invoiceninja
rm -f /etc/nginx/sites-enabled/default
nginx -t
systemctl restart nginx

7. Initialize the database and administrator

cd /var/www/invoiceninja
runuser -u www-data -- env HOME=/var/www/invoiceninja php artisan optimize:clear
runuser -u www-data -- env HOME=/var/www/invoiceninja php artisan migrate --force
runuser -u www-data -- env HOME=/var/www/invoiceninja php artisan db:seed --force
runuser -u www-data -- env HOME=/var/www/invoiceninja php artisan storage:link

runuser -u www-data -- env HOME=/var/www/invoiceninja \
  php artisan ninja:create-account \
  --email='admin@example.com' --password='USE_A_NEW_LONG_RANDOM_PASSWORD'

runuser -u www-data -- env HOME=/var/www/invoiceninja php artisan optimize

Run ninja:create-account only when the database does not already contain an account. Store the administrator password in a password manager and change it after the first login if it was exposed in shell history.

8. Run the queue and scheduler with systemd

Create /etc/systemd/system/invoiceninja-queue.service:

[Unit]
Description=Invoice Ninja Laravel queue worker
Requires=mariadb.service
After=network-online.target mariadb.service php8.3-fpm.service
ConditionPathExists=/var/www/invoiceninja/artisan

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/invoiceninja
Environment=HOME=/var/www/invoiceninja
ExecStart=/usr/bin/php artisan queue:work --sleep=3 --tries=3 --timeout=120 --max-time=3600
ExecReload=/usr/bin/php artisan queue:restart
Restart=always
RestartSec=5s
KillSignal=SIGTERM
TimeoutStopSec=360s
UMask=0027

[Install]
WantedBy=multi-user.target

Create /etc/systemd/system/invoiceninja-scheduler.service:

[Unit]
Description=Run the Invoice Ninja Laravel scheduler
Requires=mariadb.service
After=mariadb.service
ConditionPathExists=/var/www/invoiceninja/artisan

[Service]
Type=oneshot
User=www-data
Group=www-data
WorkingDirectory=/var/www/invoiceninja
Environment=HOME=/var/www/invoiceninja
ExecStart=/usr/bin/php artisan schedule:run
Nice=10
UMask=0027

Create /etc/systemd/system/invoiceninja-scheduler.timer:

[Unit]
Description=Run the Invoice Ninja scheduler every minute

[Timer]
OnCalendar=*-*-* *:*:00
AccuracySec=1s
Persistent=true
Unit=invoiceninja-scheduler.service

[Install]
WantedBy=timers.target

Load and start the units:

systemctl daemon-reload
systemctl enable --now invoiceninja-queue.service
systemctl enable --now invoiceninja-scheduler.timer
systemctl status invoiceninja-queue.service --no-pager
systemctl list-timers invoiceninja-scheduler.timer --no-pager

9. Enable HTTPS and the firewall

After the hostname resolves to the server, install Certbot and request the certificate:

apt-get install -y certbot python3-certbot-nginx
certbot --nginx -d invoice.example.com

ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw enable

Confirm that APP_URL and REACT_URL exactly match the final HTTPS URL and that REQUIRE_HTTPS=true. Then clear and rebuild the application cache:

cd /var/www/invoiceninja
runuser -u www-data -- env HOME=/var/www/invoiceninja php artisan optimize:clear
runuser -u www-data -- env HOME=/var/www/invoiceninja php artisan optimize
systemctl reload invoiceninja-queue.service
systemctl reload nginx

10. Configure outgoing mail

The initial MAIL_MAILER=log setting writes messages to the application log and does not deliver them. Configure a real SMTP service in .env, for example:

MAIL_MAILER=smtp
MAIL_HOST=smtp.example.com
MAIL_PORT=587
MAIL_USERNAME=CHANGE_ME
MAIL_PASSWORD=CHANGE_ME
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=billing@example.com
MAIL_FROM_NAME="Invoice Ninja"

After changing mail settings, run php artisan optimize:clear and php artisan optimize as www-data, then restart the queue worker. Use Invoice Ninja's mail test before sending real invoices.

Health checks

curl -fsSI https://invoice.example.com/
curl -fsS -D - -o /dev/null \
  -H 'Host: invoice.example.com' http://127.0.0.1/ \
  | grep -Fi 'X-APP-VERSION: 5.13.26'

systemctl is-active mariadb php8.3-fpm nginx invoiceninja-queue.service
systemctl is-active invoiceninja-scheduler.timer
mysql invoiceninja -e 'SELECT COUNT(*) AS accounts FROM accounts;'

sudo -u www-data env HOME=/var/www/invoiceninja/storage \
  timeout 45 \
  /var/www/invoiceninja/vendor/beganovich/snappdf/versions/ungoogled/chrome-linux/chrome \
  --headless --disable-gpu --no-sandbox --disable-dev-shm-usage \
  --disable-crash-reporter --dump-dom about:blank >/dev/null

A successful result is an HTTP 200 response, the expected application-version header, active services, at least one account, and a zero exit status from the Chromium smoke test.

Routine maintenance

  • Install Ubuntu security updates regularly and reboot when required.
  • Monitor systemctl status invoiceninja-queue.service and journalctl -u invoiceninja-queue.service.
  • Review /var/www/invoiceninja/storage/logs/laravel.log for application errors.
  • Verify the scheduler with journalctl -u invoiceninja-scheduler.service.
  • Check certificate renewal with certbot renew --dry-run.
  • Watch free disk space and MariaDB growth; uploaded documents and generated PDFs can grow over time.

Backups and restoration

A usable backup must contain both the database and the files that match it. At minimum, retain:

  • A consistent MariaDB dump of the invoiceninja database.
  • /var/www/invoiceninja/.env, especially its original APP_KEY.
  • /var/www/invoiceninja/storage, including uploaded documents.
  • /etc/invoiceninja, Nginx configuration, and the three systemd unit files.

Example backup commands:

BACKUP_DIR="/root/invoiceninja-backup-$(date +%F-%H%M%S)"
install -d -m 0700 "$BACKUP_DIR"
mariadb-dump --single-transaction --routines --triggers invoiceninja \
  > "$BACKUP_DIR/invoiceninja.sql"
tar -C / -czf "$BACKUP_DIR/invoiceninja-files.tar.gz" \
  var/www/invoiceninja/.env \
  var/www/invoiceninja/storage \
  etc/invoiceninja \
  etc/nginx/sites-available/invoiceninja \
  etc/systemd/system/invoiceninja-queue.service \
  etc/systemd/system/invoiceninja-scheduler.service \
  etc/systemd/system/invoiceninja-scheduler.timer
chmod -R go-rwx "$BACKUP_DIR"

Copy the backup off the server and test restoration periodically. During restoration, deploy the matching application release, restore files and ownership, import the SQL dump, run migrations, rebuild caches, and restart the worker. Do not generate a new APP_KEY.

Updating Invoice Ninja

Read the Invoice Ninja release notes and requirements before every update. Do not assume that a future release supports the same PHP version or that the downloadable archive has the same checksum.

  1. Take and verify a complete backup.
  2. Download the intended release from the official project and verify its published checksum.
  3. Enable maintenance mode: sudo -u www-data php artisan down.
  4. Stop the queue worker and scheduler timer.
  5. Extract the new code to a separate directory. Preserve the existing .env and storage data.
  6. Apply the ownership and permissions described above.
  7. Run php artisan optimize:clear, php artisan migrate --force, and php artisan optimize as www-data.
  8. Start the timer and worker, disable maintenance mode with php artisan up, and repeat every health check.

Keep the previous application tree and database backup until the new version has processed queues and generated a test PDF successfully.

Troubleshooting

502 Bad Gateway

systemctl status php8.3-fpm nginx --no-pager
test -S /run/php/php8.3-fpm.sock
nginx -t
journalctl -u php8.3-fpm -u nginx -n 100 --no-pager

Queue jobs do not run

systemctl status invoiceninja-queue.service --no-pager
journalctl -u invoiceninja-queue.service -n 100 --no-pager
cd /var/www/invoiceninja
sudo -u www-data php artisan queue:failed
sudo -u www-data php artisan queue:restart

Scheduled tasks do not run

systemctl list-timers invoiceninja-scheduler.timer --all
journalctl -u invoiceninja-scheduler.service -n 100 --no-pager
cd /var/www/invoiceninja
sudo -u www-data php artisan schedule:list
sudo -u www-data php artisan schedule:run -v

Invoices or PDFs fail to render

Run the Chromium smoke test from the health-check section as www-data. Confirm that SNAPPDF_EXECUTABLE_PATH exists and is executable, then inspect the Laravel log. A missing Chromium library normally appears in the command's terminal output.

Permission errors

chown -R www-data:www-data /var/www/invoiceninja/storage \
  /var/www/invoiceninja/bootstrap/cache
find /var/www/invoiceninja/storage /var/www/invoiceninja/bootstrap/cache \
  -type d -exec chmod 0775 {} +
chmod 0600 /var/www/invoiceninja/.env

Configuration changes appear to be ignored

cd /var/www/invoiceninja
sudo -u www-data php artisan optimize:clear
sudo -u www-data php artisan optimize
systemctl restart invoiceninja-queue.service
Security reminders: keep APP_DEBUG=false, restrict .env and backup permissions, use HTTPS, patch the operating system, and never change the established APP_KEY during normal maintenance.

Ingaba le mpendulo ibe luncedo?
Back