Skip to content

Install on your own server

Updated

Fill in your own values. Every command on this page updates as you type.

One command

On a fresh Ubuntu 22.04 or 24.04, Debian 12 or 13, AlmaLinux 9 or Rocky Linux 9 server, one command does the server work of this page (everything but HTTPS, and the administrator account you create in the wizard):

curl -fsSL https://raw.githubusercontent.com/Panelica/pnlcs/main/install.sh | sudo bash -s -- --domain billing.example.com

It installs nginx, PHP 8.4 and MySQL or MariaDB, downloads the latest release and checks its signature against the PNLCS release key, creates the database, and sets up nginx, the scheduler, the firewall and (on AlmaLinux and Rocky) SELinux. It ends by printing the address of the install wizard, where you create your administrator account. Leave out --domain to use the server's IP address.

  • It stops before changing anything if /var/www/pnlcs, the database or the nginx site already exists: it never touches an installation.
  • The database password is in /root/pnlcs-install.txt (readable by root only), the full log in /var/log/pnlcs-install.log.
  • --db-password (with --db-name, --db-user, --db-host) uses a database you created instead of installing a database server; --version installs another release; --help lists every option.
  • HTTPS is not set up for you: follow step 15 once the domain points at the server.

Run on freshly created servers, from the command to signing in as the administrator:

System PHP Database Result
Ubuntu 22.04 LTS 8.4 (ondrej PPA) MySQL 8.0 installed, wizard completed, scheduler runs
Ubuntu 24.04 LTS 8.4 (ondrej PPA) MySQL 8.0 installed, wizard completed, scheduler runs
Debian 12 8.4 (sury) MariaDB 10.11 installed, wizard completed, scheduler runs
Debian 13 8.4 (Debian) MariaDB 11.8 installed, wizard completed, scheduler runs
AlmaLinux 9 8.4 (Remi) MySQL 8.0 installed with SELinux enforcing (no denials), wizard completed, scheduler runs
Rocky Linux 9 8.4 (Remi) MySQL 8.0 installed with SELinux enforcing (no denials), wizard completed, scheduler runs

The rest of this page is the same installation done by hand.

Step by step

A plain Laravel installation: PHP-FPM 8.4, MySQL or MariaDB, and Nginx. Every command below was run, in this order, on freshly created servers:

System PHP Database Result
Ubuntu 24.04 LTS 8.4 (ondrej PPA) MySQL 8.0 installed, wizard completed, scheduler and update tested
Debian 13 8.4 (Debian) MariaDB 11.8 installed, wizard completed, scheduler and update tested
AlmaLinux 9 8.4 (Remi) MySQL 8.0 installed with SELinux enforcing (no denials), wizard completed, scheduler and update tested

Rocky Linux 9 uses the same commands as AlmaLinux 9.

The one rule that prevents most install problems

The whole PNLCS directory belongs to the web server user, and every composer, php artisan and npm command runs as that user. The install wizard writes .env, the application writes storage/, and updates run git, composer and npm inside the tree. When the tree is owned by root you get a 500 on the last step of the wizard (.env not writable), fatal: detected dubious ownership from git pull, and EACCES from npm.

The values that differ between the two families of systems:

Ubuntu / Debian AlmaLinux / Rocky
Web server user www-data apache
PHP-FPM socket /run/php/php8.4-fpm.sock /run/php-fpm/www.sock
PHP-FPM service php8.4-fpm php-fpm
Database service mysql (Ubuntu), mariadb (Debian) mysqld
Nginx site file /etc/nginx/sites-available/pnlcs /etc/nginx/conf.d/pnlcs.conf
SELinux not used enforcing: step 10 is required

The guide installs into /var/www/pnlcs and uses example.com as the address. Replace both with your own.

0. Prepare the server

Skip this if PHP 8.4, a database, Node and Composer are already installed (a control panel such as Panelica gives you all of them — see Installing inside a hosting-panel account).

Ubuntu 24.04

Ubuntu 24.04 ships PHP 8.3, which is not enough, so PHP 8.4 comes from the ondrej PPA.

sudo apt update
sudo apt install -y software-properties-common git unzip curl cron
sudo add-apt-repository -y ppa:ondrej/php
sudo apt update
sudo apt install -y php8.4-fpm php8.4-cli php8.4-mysql php8.4-mbstring \
  php8.4-xml php8.4-curl php8.4-zip php8.4-gd php8.4-bcmath php8.4-intl php8.4-imap
sudo apt install -y mysql-server nginx

# Node.js 20 LTS (only for building the frontend assets)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

Debian 13

Debian 13 carries PHP 8.4 itself — no extra repository. Two differences from Ubuntu, both found on a clean Debian 13 server:

  • There is no php8.4-imap package. Leave it out: if you put it in the list, apt refuses the whole line and installs no PHP at all. imap is optional (mailbox → ticket import only).
  • There is no mysql-server package. Debian ships MariaDB, which PNLCS supports.
  • cron is not installed on the Debian 13 cloud image — without it the scheduler in step 13 never runs. It is in the list below.
sudo apt update
sudo apt install -y git unzip curl cron
sudo apt install -y php8.4-fpm php8.4-cli php8.4-mysql php8.4-mbstring \
  php8.4-xml php8.4-curl php8.4-zip php8.4-gd php8.4-bcmath php8.4-intl
sudo apt install -y mariadb-server nginx

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

AlmaLinux 9 / Rocky Linux 9

PHP 8.4 comes from the Remi repository. The zip extension is a separate package here (php-pecl-zip), and unlike Debian-based systems the services are not started for you.

sudo dnf install -y epel-release https://rpms.remirepo.net/enterprise/remi-release-9.rpm
sudo dnf module reset php -y
sudo dnf module enable php:remi-8.4 -y
sudo dnf install -y php php-fpm php-mysqlnd php-mbstring php-xml php-gd \
  php-bcmath php-intl php-imap php-pecl-zip \
  mysql-server nginx git unzip cronie policycoreutils-python-utils

curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
sudo dnf install -y nodejs

curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

# Start the services now and on every boot.
sudo systemctl enable --now php-fpm mysqld nginx

Check what you have:

php -v          # PHP 8.4.x — 8.3 will 500 at runtime
php -m | grep -E '^(bcmath|curl|dom|fileinfo|gd|intl|mbstring|openssl|pdo_mysql|tokenizer|xml|zip)$' | wc -l   # 12
mysql --version # MySQL 8.0 / MariaDB 10.6 or newer
node -v         # v20.19+ or v22.12+
composer -V     # Composer version 2.x

Firewall. The cloud images we tested had no firewall enabled. If yours does, open HTTP and HTTPS — sudo ufw allow 'Nginx Full' on Ubuntu, or sudo firewall-cmd --permanent --add-service=http --add-service=https && sudo firewall-cmd --reload on AlmaLinux/Rocky.

1. Get the code and hand it to the web server user

sudo git clone https://github.com/Panelica/pnlcs.git /var/www/pnlcs
sudo chown -R www-data:www-data /var/www/pnlcs     # AlmaLinux/Rocky: apache:apache
cd /var/www/pnlcs

Define a short helper for the rest of this guide. It runs a command as the web server user, with a writable home for the Composer and npm caches (the web user's own home, /var/www, belongs to root):

# Ubuntu / Debian
pn() { sudo -u www-data HOME=/tmp/pnlcs-home COMPOSER_HOME=/tmp/pnlcs-home/composer "$@"; }

# AlmaLinux / Rocky
pn() { sudo -u apache HOME=/tmp/pnlcs-home COMPOSER_HOME=/tmp/pnlcs-home/composer "$@"; }

The helper only lives in your current shell — define it again if you log in later.

2. Install PHP dependencies

pn composer install --no-dev --optimize-autoloader --no-interaction

AlmaLinux/Rocky: sudo there does not search /usr/local/bin, so the line above answers sudo: composer: command not found. Use the full path: pn /usr/local/bin/composer install --no-dev --optimize-autoloader --no-interaction

3. Create the database

Open the database console as root — on a fresh server root signs in through the system account, so there is no password to type:

sudo mysql

Then create the database and a user for PNLCS (MySQL and MariaDB accept the same statements):

CREATE DATABASE pnlcs CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'pnlcs'@'localhost' IDENTIFIED BY 'choose-a-strong-password';
GRANT ALL PRIVILEGES ON pnlcs.* TO 'pnlcs'@'localhost';
FLUSH PRIVILEGES;
EXIT;

4. Configure the environment

pn cp .env.example .env
sudo nano .env

Set at least these values (editing the existing file keeps its owner):

APP_NAME="Your Company"
APP_URL=https://example.com
APP_ENV=production
APP_DEBUG=false

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=pnlcs
DB_USERNAME=pnlcs
DB_PASSWORD=choose-a-strong-password

MAIL_FROM_ADDRESS="[email protected]"
MAIL_FROM_NAME="Your Company"

Then make .env readable by the web user only — it holds your database password and application key, and the install wizard must be able to write it:

sudo chown www-data:www-data .env     # AlmaLinux/Rocky: apache:apache
sudo chmod 640 .env

Notes - Keep DB_CONNECTION=mysql for MariaDB too; do not switch to sqlite (some migrations use MySQL-specific SQL). - If the site starts on plain HTTP while you set up TLS, use http:// in APP_URL for now and change it in step 15. - Mail server settings (SMTP host, user, password) are entered in the admin panel later, not here.

5. Generate the application key

pn php artisan key:generate

6. Run the database migrations

pn php artisan migrate --force

Do not run php artisan db:seed here. The install wizard you will open in step 12 runs the seeder itself and renames the seeded administrator to the username and password you choose. Seeding by hand creates an administrator first - and the wizard, seeing one, locks itself before you ever reach it, leaving you with an admin account whose password only the seeder's output shows.

The wizard's seeding provides what an installation starts with: four starter currencies (USD, EUR, GBP, TRY), VAT rules for European countries, ticket departments and statuses, 32 email templates, 30 languages, domain pricing and default homepage sections. The knowledge base articles arrive with the migrations in this step, and the interface texts ship in lang/, so neither depends on the seeder.

Headless installs only: if you are scripting an installation with no browser step at all, php artisan db:seed --force is how you seed - the administrator is then admin, with a random password the seeder prints once (set PNLCS_ADMIN_PASSWORD beforehand to choose it). The wizard stays closed by design, and signing in to change that password is your first job.

7. Build the frontend assets

pn npm ci
pn npm run build
pn php artisan storage:link

9. Cache configuration, routes and views

pn php artisan optimize

10. SELinux (AlmaLinux / Rocky only)

SELinux is enforcing on AlmaLinux and Rocky. Without these lines every page answers 500: PHP-FPM may not write to storage/, and may not connect to the database. We measured exactly those two denials in the audit log of a clean AlmaLinux 9 server. The mail lines below came from a clean Rocky Linux 9 server with Postfix, sending the test email with PHP mail.

sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/pnlcs/storage(/.*)?"
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/pnlcs/bootstrap/cache(/.*)?"
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/pnlcs/\.env"
sudo restorecon -R /var/www/pnlcs

sudo setsebool -P httpd_can_network_connect_db 1   # PHP → MySQL/MariaDB
sudo setsebool -P httpd_can_network_connect 1      # PHP → payment gateways, server and registrar APIs, SMTP
sudo setsebool -P httpd_can_sendmail 1             # PHP mail: the local sendmail (Postfix)

With PHP mail (Setup → General → Mail) the mail goes to the local sendmail. Laravel's default sendmail -bs has Postfix start its own SMTP daemon, which SELinux does not allow from a web request even with httpd_can_sendmail; set this in .env so sendmail reads the message as it is (the one-command installer does both):

MAIL_SENDMAIL_PATH="/usr/sbin/sendmail -t -i"

Without them the test email on that page fails with Connection to "process /usr/sbin/sendmail -bs -i" has been closed unexpectedly. Mail sent by the scheduler is not confined and goes out either way, so the failure shows only on what a web request sends.

Do not switch SELinux off to make the errors go away — these rules give the application exactly what it needs and nothing more.

11. Point Nginx at public/

The document root must be the public/ directory, never the project root — pointing it at the project root exposes .env.

Ubuntu / Debian — create /etc/nginx/sites-available/pnlcs:

server {
    listen 80;
    server_name example.com;
    root /var/www/pnlcs/public;      # note: /public

    index index.php;
    charset utf-8;
    client_max_body_size 64M;        # ticket + backup uploads

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

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

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

Enable it and remove the default site — otherwise a request to the server's IP address keeps landing on the "Welcome to nginx" page:

sudo ln -s /etc/nginx/sites-available/pnlcs /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx

AlmaLinux / Rocky — create /etc/nginx/conf.d/pnlcs.conf with the same block, changing only the PHP-FPM socket:

        fastcgi_pass unix:/run/php-fpm/www.sock;
sudo nginx -t && sudo systemctl reload nginx

Check it before you open a browser:

curl -sI http://example.com/install | head -1      # HTTP/1.1 302 Found  (→ the wizard)
curl -sI http://example.com/.env | head -1         # 403 or 404 — never 200

Using Apache or Caddy instead? The same rule applies: document root = public/, PHP handled by PHP-FPM 8.4, and every request that is not a file rewritten to index.php.

12. Run the install wizard

Open http://example.com/install in your browser. The wizard walks through:

The language switch at the top of the wizard offers English, Türkçe, Deutsch, Polski and 中文; the language you pick there is also preselected as the default language on the last step.

  1. Requirements — PHP 8.4, every required extension, whether storage/ (with framework/ and logs/), bootstrap/cache/ and .env are writable, and whether vendor/ and public/build/ exist (steps 2 and 7). Everything must be green before Continue appears; a red line names exactly what to fix.
  2. Database — skipped automatically when the migrations from step 6 have all run. If some are still pending, the wizard runs them first.
  3. Administrator — the username, first name, email and password you choose (at least 6 characters). There is no default password to change afterwards.
  4. Application — the public URL, the Application Name (written to APP_NAME in .env) and the default language, one of 12.
  5. Finish — the wizard writes a lock file and closes itself permanently; /install answers 404 from then on.

Sign in at http://example.com/admin/login.

13. Schedule the cron runner

Everything that happens by itself — invoice generation, payment reminders, suspensions, SSL polling, backups, queued mail — is driven by one cron line for the web server user:

sudo crontab -u www-data -e     # AlmaLinux/Rocky: -u apache
* * * * * cd /var/www/pnlcs && php artisan schedule:run >> /dev/null 2>&1

Check that the schedule is there and runs cleanly:

pn php artisan schedule:list
pn php artisan schedule:run

14. Queue: no separate worker needed

.env.example ships QUEUE_CONNECTION=sync: mail and background jobs run inline, which is the right shape for a single server.

If you switch to QUEUE_CONNECTION=database, the cron line from step 13 already processes the queue: the scheduler starts a worker every minute that drains the queue and stops. We verified this on a clean server — a queued email was in the jobs table before schedule:run, and sent after it, with no supervisor installed. Jobs therefore wait up to a minute.

Add a permanent worker only if you want queued jobs to run instantly. A supervisor entry for that:

[program:pnlcs-worker]
command=php /var/www/pnlcs/artisan queue:work database --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
user=www-data

15. Turn on HTTPS

Checkout and the admin login must never run over plain HTTP. With Certbot (Ubuntu/Debian: sudo apt install -y certbot python3-certbot-nginx):

sudo certbot --nginx -d example.com

Then make sure APP_URL in .env starts with https:// and refresh the cache:

pn php artisan optimize

16. Final check

curl -sI https://example.com/install | head -1   # 404 — the wizard is closed
curl -sI https://example.com/.env | head -1      # 403 or 404
ls -l /var/www/pnlcs/.env                                  # owned by the web user, mode 640
pn php artisan schedule:run                                # finishes without errors

Then continue with First Login and the Setup Checklist.