Open-source, self-hosted hosting billing platform β a free WHMCS alternative.
Client portal Β· invoicing Β· domain & SSL management Β· support tickets Β· reseller hosting.
Customers manage their hosting from the billing portal itself β files, mail,
databases, FTP, subdomains, DNS, cron and backups.
Built with Laravel 13 Β· PHP 8.4+ Β· MySQL 8 Β· Alpine.js Β· Tailwind CSS 4
π Try the live demo: hosting.panelica.com
π Read the documentation: docs.pnlcs.com β user guide, the full API reference and the MCP server
Website Β· Live Demo Β· Documentation Β· Docker Β· Installation Β· First Steps Β· Screenshots Β· Features Β· Modules Β· Contributing
PNLCS is a free, open-source, self-hosted hosting billing platform and client portal β an open alternative to WHMCS for web hosting companies, reseller hosts, and infrastructure providers. It covers the full customer lifecycle: product catalog, checkout, recurring invoicing, domain registration, SSL certificate management, support tickets, knowledge base, and affiliate tracking.
If you have used WHMCS, you will feel at home: the data model, workflows, and module ecosystem (servers, gateways, registrars, SSL providers) are deliberately familiar. The difference is that PNLCS is MIT-licensed, self-hosted, and free to fork, study, and extend.
This project is built and maintained by the Panelica Server Management Panel team in our spare time, alongside our main product. We wanted an open, self-hosted billing system that integrates natively with Panelica and plays nicely with other control panels too β so we built one.
What works well today:
- Client portal, admin panel, and core billing flows
- Invoicing, orders, services, domains, tickets, knowledge base
- The Panelica server module (fully tested against live servers)
- Stripe payment gateway (tested in production)
- Multi-language UI (30 locales, admin-editable translations)
What needs your help:
- cPanel / Plesk / DirectAdmin / Proxmox server modules β code is in place but we have not tested them end-to-end. If you run one of these, please try it out and open an issue (or even better, a pull request) telling us what you found.
- PayPal / Authorize.Net / Bank Transfer gateways β same story.
- Enom domain registrar β API integration exists, needs real-world verification.
- General bug reports, typos, translation improvements.
We read every issue, but because this is a side project our response time isn't always same-day. If you can include reproduction steps or a patch, it helps us enormously.
PNLCS is developed and maintained by the team behind Panelica, a modern server management panel for web hosting β a fresh alternative to cPanel, Plesk, and CyberPanel.
Why Panelica?
- No CloudLinux required. Per-user isolation β CPU, RAM, I/O, and process limits β is built in natively via cgroups v2, Linux namespaces, per-user PHP-FPM pools, and SSH chroot. You get CageFS/LVE-style tenant isolation without paying for a separate CloudLinux license.
- Universal migration. Move whole accounts in from cPanel, Plesk, DirectAdmin, and CyberPanel β sites, databases, emails, DNS, and SSL β with file and database hashes preserved, so passwords and configs keep working.
- Affordable. A modern, fully isolated hosting stack at a fraction of the typical cPanel + CloudLinux bill.
- All-in-one, isolated stack. Nginx, Apache, multiple PHP versions, PostgreSQL, MySQL, Redis, BIND, mail (Postfix/Dovecot), FTP, ClamAV, fail2ban, and ModSecurity β each service isolated and managed from one panel.
PNLCS integrates natively with Panelica through the built-in Panelica server module: sell hosting plans and accounts are provisioned on your Panelica servers automatically β and the customer then manages that hosting (files, mailboxes, databases, FTP, subdomains, DNS, cron, backups) from the billing portal itself, with every action fenced to their own account. See Hosting Management from Inside Billing.
Not only Panelica. PNLCS is control-panel agnostic β you can connect and provision on cPanel, Plesk, DirectAdmin, HestiaCP, Proxmox, and Vultr too, right alongside your Panelica servers. Mix and match panels in a single install; Panelica is simply where PNLCS feels most at home.
π Learn more at panelica.com
Admin dashboard with revenue, orders, and ticket overview
Built-in translation editor β 30 locales, 2,232 translation keys, AI-assisted bulk translate
WordPress-style theme system with 15 built-in themes, logo/favicon upload, dark mode toggle, and homepage builder
Server module management β connect Panelica, cPanel, Plesk, DirectAdmin, Proxmox, or custom servers
Ticket system with departments, priority routing, internal notes, and escalation rules
Customer-facing landing page with domain search, hosting plans, VPS servers, and FAQ β fully configurable
Same site with a different built-in theme applied β one click to switch
The app showcase: 98 applications a customer can install into their hosting,
with the logos shipped in the repository. Heading, copy, button and how many
apps to show are all editable from the admin Homepage screen
A customer's hosting service: live resource usage from the server and eight
working tools β files, mail, databases, FTP, subdomains, DNS, cron, backups.
Full detail below
Restore points with size, contents and encryption state β fenced to the
customer's own domains
Installing an app from the customer's own control panel: searchable, grouped
the way people shop, and every card states the memory the app needs and how
many containers it starts. An app that wants more than the plan allows is
marked before it is chosen, not after it fails
- Shop & checkout β browse plans, configure service, apply coupons, pay
- Service management β upgrade, downgrade, cancel, auto-renew toggle
- Domain management β register, transfer, renew, EPP code, WHOIS
- Invoicing β view, pay online, download PDF, add-funds, credit balance
- Quotes β review, accept (auto-converts to invoice) or decline pre-sales quotes
- Payment methods β save bank-transfer references, set a default
- Bank-transfer notifications β report an offline payment with receipt upload
- Email history β read every email the system has sent you
- Network status β live view of active incidents and scheduled maintenance
- Support tickets β attachments, priority, department routing
- Knowledge base & announcements β searchable, categorized
- SSL certificates β CSR generation, approver emails, auto-install
- Affiliate program β referral tracking, commission payouts
- Account security β 2FA (TOTP), login alerts, session history
- Hosting management β files, mailboxes, databases, FTP, subdomains, cron, DNS and backups, without leaving billing (details below)
- Dashboard β revenue, new signups, pending orders, open tickets at a glance
- Client management β profiles, impersonation, notes, billing summary
- Orders & invoices β manual create, bulk actions, mass mail, PDF export
- Products & bundles β configurable options, addons, pricing matrices
- Ticket system β internal notes, escalation rules, spam filter
- Reports β revenue, conversion funnel, MRR, churn, affiliate stats
- Bulk operations β mass email, bulk invoice, bulk service status update
- Calendar β events, reminders, scheduled tasks
- Quotes & projects β WHMCS-style pre-sales flow
- 30 locales, English active by default
- 2,232 translation keys for the core UI
- In-browser editor β edit strings without touching files
- AI-assisted bulk translate for missing keys
- Export / import JSON per locale
- 15 built-in themes (Arctic, Aurora, Coral, Ember, Forest, Midnight, Mint, Neon, Ocean, Panelica, Royal, Slate, Starter, Sunset, and more)
- WordPress-style install / activate / delete workflow
- Homepage builder with reorderable sections
- Per-site white-label options (logo, favicon, footer copyright)
- Dark mode toggle per theme
- RBAC with over 45 fine-grained permissions
- 2FA (TOTP) for both admin and client portals
- Rate-limited login, 2FA verify, password reset, email resend
- IP whitelisting for admin area (optional)
- Session timeout and force logout support
- Activity log β every admin action recorded
- Banned IPs & emails at the application level
- Recurring billing β monthly, quarterly, semi-annually, annually, biennially
- Auto-suspend unpaid services after configurable grace period
- Late fees, promotions, coupons, tax rules (inclusive/exclusive)
- Overage billing β disk / bandwidth metering, opt-in per product
- Credit balances & add-funds flow
- Automated reminders β invoice, payment, CC expiry, domain renewal
- Auto-renew services and domains with billing integration
- Unified payment engine β one path for gateways, manual and credit, with partial-payment and overpayment-to-credit handling
- Reliable provisioning β a service only activates after the server module succeeds; failures are queued and retried automatically with admin alerts
- Refunds β reverse a payment via the gateway API (or offline), full or partial
- Exchange-rate auto-update, automated database backups, and log retention on a schedule
- REST API with API-key auth for external integrations
- Webhooks β inbound (gateway callbacks) and outbound (events)
- Queue workers for email and background jobs
- Scheduled commands β invoice generation, reminders, polling
- Hook system β WHMCS-compatible
add_hook()/run_hook()with 20+ hook points; a failing hook can never break billing or provisioning - Email piping β inbound IMAP/POP3 mailboxes turn emails into tickets and replies
- Modular architecture β add server / gateway / registrar modules without touching core
- Eloquent everywhere β no raw SQL, no string concatenation
Most billing platforms stop at "here is your control panel password." PNLCS does the day-to-day hosting work in the billing portal itself, so a customer who wants to add a mailbox or a DNS record never has to learn a second interface.
A hosting service in the client portal β live CPU/memory/disk/bandwidth from the
server, and eight working tools underneath
Available on services provisioned through the Panelica server module (the module tells the portal which tools that account may use):
| Tool | What the customer can do |
|---|---|
| File Manager | Browse, upload, download, edit, rename, create folders and delete |
| Email Accounts | Create and delete mailboxes, change passwords, open webmail |
| Databases | Create MySQL databases and users, reset user passwords, open phpMyAdmin |
| FTP Accounts | Create accounts, change passwords, delete β with host/port shown |
| Subdomains | Create and remove subdomains; the panel provisions the real vhost, document root, PHP-FPM pool, SSL and DNS |
| DNS Zone | Add, edit and delete A / AAAA / CNAME / MX / TXT / SRV / CAA records |
| Cron Jobs | Schedule commands, run one immediately and read its output, pause/resume, delete |
| Backups | Take restore points, see size and contents, delete old ones |
The server API key a billing platform holds is operator-wide β it can see every account on the box. That is exactly the mistake this integration does not make: every list is filtered against the domains that belong to the service being viewed, and every write is checked the same way before it is sent.
- A backup archive that also covers somebody else's domain is not shown, and cannot be deleted.
- A cron job, subdomain or DNS record on a foreign domain is rejected before a request leaves the billing server.
- Plan limits are read from the customer's hosting plan β
max_subdomains,max_cron_jobs,cron_jobs_enabled,backup_enabledβ and the create form is gated on them (the panel enforces them again, independently).
One zone at a time, records ordered the way an operator reads them, and the
delegation and apex records locked
A zone editor in a billing panel is a fast way for a customer to take their own
site offline. So the records the hosting itself depends on β SOA, NS, and the
apex/www A records pointing at the server β are shown as Managed and
cannot be edited, renamed into, or deleted here. Renaming an ordinary record
into one of those names is blocked too. The hosting panel remains the place to
override that deliberately.
The same restraint applies elsewhere: restoring a backup is not offered in billing (it silently discards everything written since the archive was taken), and cron commands run as the account's own unprivileged system user inside the panel's namespace and cgroup isolation β never as root.
Common schedules or a full five-field expression, with example commands that
fill in the customer's real domain path
| Component | Minimum |
|---|---|
| PHP | 8.4 (the locked Symfony 8 dependencies require it β 8.3 installs, then answers every request with a 500) |
| Database | MySQL 8.0 or MariaDB 10.6+ |
| Node.js | 18+ (20 LTS recommended) β only needed to build the frontend assets |
| Composer | 2.x |
| Web server | Nginx or Apache with PHP-FPM |
| Tools | git, unzip, curl, cron |
| Disk | ~130 MB for the app itself (code + PHP dependencies + built assets); the Docker image is ~410 MB. Allow at least 2 GB free for the database, ticket/backup uploads and logs as they grow. node_modules (~100 MB) is only needed while building and can be removed afterwards. |
| RAM | 1 GB works for a small install; 2 GB is comfortable with the database on the same box |
| PHP extensions (required) | bcmath, curl, dom, fileinfo, gd, intl, mbstring, openssl, pdo_mysql, tokenizer, xml, zip β the install wizard checks each one |
| PHP extensions (optional) | imap β only for turning emails in a mailbox into support tickets (Setup β Ticket Departments β mail import). Everything else works without it. |
Optional but recommended: an SMTP server or relay for email delivery, Redis for session/cache, and a TLS certificate (Let's Encrypt) before you take payments.
The fastest way to try PNLCS is the official Docker image
panelica/pnlcs-runtime.
It bundles PHP-FPM 8.4 + nginx + Node.js 20 + supervisor and clones the latest
code from this repository on first start. No manual composer install or
npm run build β the entrypoint handles everything.
docker network create pnlcs-net
docker run -d --name pnlcs-db --network pnlcs-net \
-e MYSQL_ROOT_PASSWORD=changeme \
-e MYSQL_DATABASE=pnlcs -e MYSQL_USER=pnlcs -e MYSQL_PASSWORD=changeme \
mariadb:11
docker run -d --name pnlcs --network pnlcs-net -p 8090:80 \
-e DB_HOST=pnlcs-db -e DB_DATABASE=pnlcs \
-e DB_USERNAME=pnlcs -e DB_PASSWORD=changeme \
-e APP_URL=http://localhost:8090 \
panelica/pnlcs-runtime:1.4Wait 3β5 minutes for the first start (composer install + npm build), then visit http://localhost:8090/install to run the in-app install wizard. The wizard guides you through requirements check, admin account creation (you choose username + password), and application settings β then locks itself permanently.
To pull the latest code from this repo into a running container:
docker exec pnlcs /usr/local/bin/update.shπ¦ Full image documentation, environment variables, screenshots, and production deployment notes: π https://hub.docker.com/r/panelica/pnlcs-runtime
This is a plain Laravel install on your own server: PHP-FPM, MySQL or MariaDB, and Nginx. Every command below was run, in this order, on freshly created servers:
| Operating system | PHP | Database | Result |
|---|---|---|---|
| Ubuntu 24.04.5 LTS | 8.4.25 (ondrej PPA) | MySQL 8.0.46 | β installed, wizard completed, scheduler and update tested |
| Debian 13 (trixie) | 8.4.24 (Debian) | MariaDB 11.8.6 | β installed, wizard completed, scheduler and update tested |
| AlmaLinux 9.8 | 8.4.26 (Remi) | MySQL 8.0.46 (AppStream) | β installed with SELinux enforcing (0 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 artisanandnpmcommand runs as that user. The install wizard writes.env, the application writesstorage/, and updates rungit,composerandnpminside the tree. When the tree is owned byrootyou get a 500 on the last step of the wizard (.envnot writable),fatal: detected dubious ownershipfromgit pull, andEACCESfromnpmβ we reproduced all three by following the old instructions on a clean Ubuntu server.
The values that differ between the two families, used throughout this guide:
| Ubuntu / Debian | AlmaLinux / Rocky | |
|---|---|---|
| Web server user | www-data |
apache (PHP-FPM runs as apache, Nginx can reach its socket) |
| 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 uses /var/www/pnlcs as the install directory and
example.com as the address β replace both with your own.
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/composerDebian 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-imappackage. Leave it out: if you put it in the list,aptrefuses the whole line and installs no PHP at all.imapis optional (mailbox β ticket import only). - There is no
mysql-serverpackage. Debian ships MariaDB, which PNLCS supports. cronis 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/composerAlmaLinux 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 nginxCheck 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 # v18 or newer
composer -V # Composer version 2.xFirewall. 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.
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/pnlcsDefine 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.
pn composer install --no-dev --optimize-autoloader --no-interactionAlmaLinux/Rocky:
sudothere does not search/usr/local/bin, so the line above answerssudo: composer: command not found. Use the full path:pn /usr/local/bin/composer install --no-dev --optimize-autoloader --no-interaction
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 mysqlThen 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;pn cp .env.example .env
sudo nano .envSet 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="noreply@your-domain.com"
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 .envNotes
- Keep
DB_CONNECTION=mysqlfor MariaDB too; do not switch tosqlite(some migrations use MySQL-specific SQL). - If the site starts on plain HTTP while you set up TLS, use
http://inAPP_URLfor now and change it in step 15. - Mail server settings (SMTP host, user, password) are entered in the admin panel later, not here.
pn php artisan key:generatepn php artisan migrate --forceDo 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 a default admin / admin123 account you never chose.
The wizard's seeding provides everything an installation starts with: four starter currencies (USD, EUR, GBP, TRY), ticket departments and statuses, 25 email templates, 30 languages, the full translation set, the knowledge base, and default homepage sections.
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
default administrator is then admin / admin123, the wizard stays closed
by design, and changing that password is your first job.
pn npm ci
pn npm run buildpn php artisan storage:linkpn php artisan optimizeSELinux 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.
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, SMTPDo not switch SELinux off to make the errors go away β these rules give the application exactly what it needs and nothing more.
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 nginxAlmaLinux / 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 nginxCheck 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 200Using 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.
Open http://example.com/install in your browser. The wizard walks through:
- Requirements β PHP version, every required extension, and whether
storage/,bootstrap/cache/and.envare writable. Everything must be green before Continue appears; a red line names exactly what to fix. - Database β skipped automatically, because you already ran the migrations in step 6.
- Administrator β the username, email and password you choose. There is no default password to change afterwards.
- Application β the public URL, the company name and the default language.
- Finish β the wizard writes a lock file and closes itself permanently;
/installanswers 404 from then on.
Sign in at http://example.com/admin/login.
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.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-dataCheckout 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.comThen make sure APP_URL in .env starts with https:// and refresh the
cache:
pn php artisan optimizecurl -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 errorsThen continue with First Steps After Installation.
If the server runs a control panel, you do not need root or any of step 0 - the panel already carries PHP, MySQL and (on Panelica) Node. This is the exact shape our own production installs use:
- Create the hosting account and its domain in the panel, and set the
domain's PHP version to 8.4 - this is the step that bites: if the
site's PHP-FPM stays at 8.3 while you ran composer with a 8.4 CLI, every
request answers
500 - Composer detected issues in your platform. - Create the MySQL database and user from the panel. Panels prefix names
(
account_pnlcs) - put the prefixed name in.env. - As the account user, clone into the site directory - next to the
webroot, not inside it:
cd ~/example.com && git clone https://github.com/Panelica/pnlcs.git pnlcs composer install,.env,key:generate,migrate --forceas in steps 2-6, using the panel's PHP 8.4 binary (on Panelica:php84). If MySQL listens on a socket, addDB_SOCKET=with the panel's socket path.- Build the assets with the panel's Node (on Panelica, install one under
Node.js Versions and use its
npm). - Point the webroot at
pnlcs/publicwith a same-owner symlink:mv public_html public_html.default && ln -s pnlcs/public public_html. A symlink owned by the same account passes the panel'sdisable_symlinks if_not_ownerprotection. - Add the cron line from step 13 as a panel cron job for the account.
- Open
https://example.com/installand finish the wizard.
PNLCS updates in place β latest code, database migrations, and rebuilt frontend assets β without touching your data.
One command pulls the latest release into a running container:
docker exec pnlcs /usr/local/bin/update.shIt runs git reset --hard origin/main β composer install β php artisan migrate
β npm run build β cache rebuild β php-fpm reload. Your database and uploaded
files live on the pnlcs_app volume and are left untouched.
Set AUTO_UPDATE=1 on the container to pull the latest code automatically on
every restart.
The web server's PHP-FPM is older than the PHP that ran composer install.
The dependencies are locked against PHP 8.4, so the page dies before Laravel
even boots - and because it dies that early, storage/logs stays empty.
Point the site (or pool) at PHP 8.4: on a panel, change the domain's PHP
version; on raw nginx, fix the fastcgi_pass socket.
If update.sh stops with:
fatal: detected dubious ownership in repository at '/var/www/pnlcs'
Git is refusing to run because the code directory is owned by a different user
than the one running the update (a normal effect of the pnlcs_app volume).
Mark the directory as trusted once β the exception is permanent, so later
updates run cleanly:
docker exec pnlcs git config --global --add safe.directory /var/www/pnlcs
docker exec pnlcs /usr/local/bin/update.shAlready inside the container shell (/var/www/pnlcs #)? Run it without
docker exec:
git config --global --add safe.directory /var/www/pnlcs
/usr/local/bin/update.shThe update resets the working tree to
origin/main, so any manual edits made inside the container are discarded β all code is served from this repository. Keep customisations in your own fork or theme, not in the running container.
If you installed PNLCS directly on a server (see
Self-Hosted Installation above),
you update it in place β new code, migrations and rebuilt assets, your data
untouched. Run everything from the PNLCS directory as the web server user,
with the same pn helper as the installation:
cd /var/www/pnlcs
pn() { sudo -u www-data HOME=/tmp/pnlcs-home COMPOSER_HOME=/tmp/pnlcs-home/composer "$@"; } # AlmaLinux/Rocky: -u apache1. Back up and pause the app.
pn php artisan down # maintenance page for visitors
pn php artisan pnlcs:db-backup # database snapshot β storage/app/backups/db/2. Pull the latest code.
pn git pull origin main3. Update PHP dependencies. (AlmaLinux/Rocky: pn /usr/local/bin/composer β¦)
pn composer install --no-dev --optimize-autoloader --no-interaction4. Apply new database migrations.
pn php artisan migrate --force5. Rebuild the frontend assets.
pn npm ci
pn npm run build6. Rebuild the cached config, routes and views.
pn php artisan optimize7. Reload PHP so the new code goes live.
sudo systemctl reload php8.4-fpm # AlmaLinux/Rocky: sudo systemctl reload php-fpmDo not skip this one. PHP's opcode cache can keep serving the previous code
for a while after the files change; on one of our own installs that showed up
as Route [...] not defined and a 500 on the dashboard until PHP-FPM was
reloaded.
8. Bring the app back up.
pn php artisan upIf you run a permanent queue worker (installation step 14), restart it too:
pn php artisan queue:restart.
We ran this exact sequence on a Debian 13 install: maintenance mode on,
pull, composer, migrations, npm ci, build, cache, reload, back live β no
errors, site answering 200 afterwards.
Errors here almost always mean the tree is not owned by the web user.
fatal: detected dubious ownership in repository from git, or EACCES from
npm, means some files belong to root β typically from an older
installation that ran commands with plain sudo. Fix it once and re-run:
sudo chown -R www-data:www-data /var/www/pnlcs # AlmaLinux/Rocky: apache:apacheThe same in-place update, but with the account's own tools instead of root β
no sudo, no systemctl. Run everything from the project directory with the
panel's PHP binary (on Panelica that is php84):
cd ~/example.com/pnlcs
php84 artisan down # maintenance page
git pull origin main
php84 /usr/local/bin/composer install --no-dev --optimize-autoloader
php84 artisan migrate --force # applies new migrations
npm ci && npm run build # the account's Node
php84 artisan optimize # rebuild cached config/routes/views
php84 artisan upThere is no PHP-reload step you run yourself: FPM picks the new code up on the
next request, or you restart PHP for the domain from the panel. If MySQL is on
a socket, the DB_SOCKET line from installation stays in .env and needs
nothing here. Your data, uploads and settings are untouched.
π New to hosting billing? The full user guide walks you through every concept and task in plain language β start with Your First Sale for an end-to-end walkthrough. The steps below are the quick version.
Once the site loads and you can reach /admin/login, do these in order:
- URL:
https://billing.your-domain.com/admin/login - Use the administrator username and password you chose in the install
wizard. (Only a headless install that seeded by hand has the default
admin/admin123- if that is you, changing it is the first job.) - (Recommended) Enable Two-Factor Authentication from My Account (the menu under your name) β Turn on.
Setup β General Settings
- Company name, support email, logo, favicon
- Default language, currency, timezone
- Date format, invoice pay terms, tax behavior
Setup β General Settings β Mail Configuration
- Choose SMTP and fill in the host, port, username, password and encryption of your mail server or relay
- Set the sender address and name
- Save, then press Send Test Email and check the inbox
These settings are stored in the database and override the MAIL_* values in
.env. Until mail is configured, .env.example's MAIL_MAILER=log is in
effect: nothing is delivered, and with the default LOG_LEVEL=warning the
messages are not written to the log either.
Setup β Appearance
- Pick a theme from the 15 built-in options
- Upload your logo and favicon
- Configure homepage sections (hero, features, pricing, testimonials)
- Set up white-label footer text
Setup β Payment Gateways
- Stripe β paste your API keys, enable
- PayPal β client ID + secret (sandbox or live)
- Bank Transfer β set instructions shown to clients
- Test each gateway with a small order before going live
Setup β Servers
- Add your Panelica / cPanel / Plesk / DirectAdmin server
- Test the API connection from the server edit page
- Assign servers to server groups if you have multiple
Products (main admin menu)
- Create a product group (e.g. "Shared Hosting")
- Create a product, link it to a server and a module
- Set pricing for monthly / quarterly / annually
- Enable auto-setup if you want provisioning on payment
Setup β Domain Pricing
- Add TLDs you sell (
.com,.net, ...) - Set registration, transfer, and renewal prices
- Link to a registrar module (or use Manual)
Setup β Tax Rules
- Add country-level or state-level tax rules
- Choose inclusive or exclusive tax display
- Assign taxable flag to products individually
Setup β Admin Roles and Setup β Admin Accounts
- Create custom roles (e.g. "Billing Manager", "Support Agent")
- Pick permissions per role from the 45+ available
- Add staff members and assign roles
Setup β General Settings β Email Verification
- Toggle Require email verification on
- New signups will receive a verification link before they can order
- Open an incognito browser
- Visit
/client/registerand create a test client - Place a test order for one of your products
- Pay with the test mode of your gateway
- Verify the invoice, service, and email flow all work
See Updating β Docker, self-hosted and hosting-panel installs each have their own short procedure there. Always back up the database before applying new migrations.
PNLCS ships with modular server, gateway, registrar, and SSL
provider integrations under the modules/ directory. Add control-panel
servers (cPanel, Plesk, DirectAdmin, Proxmox, HestiaCP, Vultr, Panelica),
configure payment gateways (Stripe, PayPal, iyzico, Authorize.Net, Razorpay,
Mollie, Tpay, bank transfer), and connect domain registrars (Enom, Namecheap,
ResellerClub, OpenProvider, HRD) β and add your own modules without touching
core code (see Writing your own module).
π‘ Choosing a panel to sell on? The Panelica server module is tested end-to-end and provisions instantly. Panelica is a modern cPanel / Plesk alternative with built-in per-user isolation (no CloudLinux) and universal migration from cPanel, Plesk, DirectAdmin, and CyberPanel.
This is every module in modules/, and whether the test suite exercises its
own code. "Covered" means there are tests that drive the module and assert on
what it sends and stores, with the provider's HTTP responses faked. It is not
a statement that the integration has been run against a live provider account.
| Module | Type | Automated tests |
|---|---|---|
| Panelica | Server | Covered |
| cPanel | Server | Covered |
| Plesk | Server | Covered |
| DirectAdmin | Server | Covered |
| Proxmox | Server | Covered |
| HestiaCP | Server | Covered |
| Vultr | Server | Covered |
| Custom | Server | None yet |
| Stripe | Gateway | Covered |
| PayPal | Gateway | Covered |
| Authorize.Net | Gateway | Covered |
| Razorpay | Gateway | Covered |
| Mollie | Gateway | Covered |
| Tpay | Gateway | None yet |
| BankTransfer | Gateway | Covered |
| Enom | Registrar | Covered |
| Namecheap | Registrar | Covered |
| ResellerClub | Registrar | Covered |
| Manual | Registrar | None yet |
| GoGetSSL | SSL | Covered |
If you run one of these against a real provider, please open an issue with what worked and what didn't β especially anything the faked responses could not have caught. A short note is enough; we can iterate from there.
Setup β Modules lists every installed server, payment gateway, registrar, SSL and addon module on one screen, each with an on/off switch:
- Switching a gateway off removes it from checkout; switching a registrar off removes it from the domain search; switching an addon off deactivates it. These are the same switches as on each module's own settings page β flipping one here or there is the same thing.
- Switching a server or SSL module off removes it from the forms that choose a module (new servers, new products). A server or SSL module that a server or product still uses cannot be switched off; the screen shows how many records use it.
- Modules you added yourself are marked Third-party.
Credentials and options stay on each type's own page (Setup β Payment Gateways, Servers, Domain Registrars, SSL Modules, and the Extensions page for addons); the Configure button on each section takes you there.
A module is a folder under modules/. Drop it in, add a pnlcs.json
manifest, and PNLCS registers it on the next request β no edit to the core,
nothing to re-register after an update, because your folder is not part of
this repository.
1. Folder layout β exactly two levels below modules/:
modules/
βββ Gateways/ β Gateways, Servers, Registrars or Ssl
βββ AcmePay/
βββ AcmePayModule.php
βββ pnlcs.json
The namespace follows the path (PSR-4, Modules\ β modules/), so the class
above is Modules\Gateways\AcmePay\AcmePayModule.
2. The manifest β pnlcs.json:
{
"name": "acmepay",
"type": "gateway",
"class": "Modules\\Gateways\\AcmePay\\AcmePayModule",
"version": "1.0.0",
"display_name": "AcmePay",
"description": "AcmePay card payments",
"author": { "name": "Your Company" }
}| Field | Required | Meaning |
|---|---|---|
name |
yes | Unique key, stored on invoices, products and settings. Lower-case, no spaces. |
type |
yes | gateway, server, registrar or ssl |
class |
yes | Fully qualified class name of the module |
version, display_name, description, author |
no | Informational |
Two rules are enforced when the manifest is read, so a broken module cannot break a working installation:
- A built-in module always wins. A manifest named
stripe(or any other built-in name) is ignored β it cannot replace a module that ships with PNLCS. - The class must be the type it claims. The class must implement the
interface for its
type(table below). A server class announced as a gateway is not registered at all, instead of failing at checkout.
A manifest that does not parse, names a class that does not exist, or leaves
out name, type or class is skipped silently.
3. The interface β implement the one for your type (app/Contracts/):
type |
Interface | Methods |
|---|---|---|
gateway |
App\Contracts\GatewayModuleInterface |
capture, refund, getPaymentForm, processWebhook, getConfigFields, getModuleName, isTokenised |
server |
App\Contracts\ServerModuleInterface |
create, suspend, unsuspend, terminate, changePassword, changePackage, usageUpdate, testConnection, getConfigFields, getModuleName |
registrar |
App\Contracts\RegistrarModuleInterface |
register, transfer, renew, getNameservers, saveNameservers, getEPPCode, getLockStatus, toggleLock, checkAvailability, getConfigFields, getModuleName |
ssl |
App\Contracts\SslModuleInterface |
purchaseCertificate, getCertificateStatus, renewCertificate, revokeCertificate, reissueCertificate, resendValidationEmail, changeValidationMethod, getApproverEmails, getWebServerTypes, getCertificateTypes, decodeCsr, generateCsr, testConnection, getConfigFields, getModuleName |
A gateway that can store a card and charge it later (automatic payment) also
implements App\Contracts\TokenizableGatewayInterface (beginVaulting,
confirmVaulting, detachStoredMethod, chargeStoredMethod); the Stripe,
iyzico and PayPal modules are complete examples.
Actions return an array with at least success (bool) and message
(string) β for example ['success' => true, 'message' => 'Account created'].
4. Settings β for gateway, registrar and SSL modules,
getConfigFields() describes the fields on the module's settings page; PNLCS
draws the form and stores the values. A gateway is only offered at checkout
once every field marked required has a value. (Server modules get their
connection details β hostname, port, username, password or API key, access
hash β from the Setup β Servers form instead.)
public function getConfigFields(): array
{
return [
['name' => 'api_key', 'label' => 'API Key', 'type' => 'password', 'required' => true],
['name' => 'mode', 'label' => 'Mode', 'type' => 'select', 'options' => ['live' => 'Live', 'test' => 'Test']],
['name' => 'debug', 'label' => 'Debug log', 'type' => 'yesno', 'default' => '0'],
['name' => 'note', 'label' => 'Note', 'type' => 'textarea'],
];
}Field types: text, password (never echoed back into the page), textarea,
select (with options) and yesno. A gateway reads its saved values from
App\Models\GatewaySettings (gateway = your name, setting = the field
name); the Mollie and Tpay modules show the pattern in a few lines.
5. Start from a working module. The simplest complete examples are
modules/Servers/Custom (a server module where every action succeeds) and
modules/Gateways/BankTransfer (an offline gateway). Copy one, rename the
folder, namespace and class, write the manifest, and open Setup β Modules
β your module appears there marked Third-party.
Current limitation β gateway webhooks. Payment-confirmation webhooks are
routed to the built-in gateways by name (/gateway/stripe/webhook,
/gateway/paypal/webhook, β¦). A third-party gateway's processWebhook() has
no public URL yet, so a gateway that confirms payments only through webhooks
cannot be completed as a drop-in module today. Redirect-and-return gateways
and server, registrar and SSL modules are not affected.
Addons (modules/Addons/<Name>/<Name>Module.php, implementing
App\Contracts\AddonModuleInterface) need no manifest: any addon folder is
listed on the Extensions page (/admin/config/addons/modules, in the
settings sidebar) and on the Modules screen, where it is activated. The Staff Board and Project Management addons are working
examples.
Tests. tests/Feature/ModuleDiscoveryTest.php shows how to exercise a
module through the same discovery the application uses. Pull requests that add
a module with tests are reviewed first.
PNLCS ships a first-party Model Context Protocol
server, pnlcs-mcp on npm. Connect
any MCP-compatible AI client β Cursor, VS Code and others β to your install and
ask it things in plain English: which invoices are overdue, any orders held
as fraud, open a ticket for this client. Fifteen read tools are always
available; the seven write tools exist only when you opt in with
PNLCS_ALLOW_WRITES=1. Zero dependencies, nothing to install on the PNLCS
side β it speaks to the same admin API your screens use, with an API
credential you create under Setup β API Credentials.
Most clients take the same JSON block:
{
"mcpServers": {
"pnlcs": {
"command": "npx",
"args": ["-y", "pnlcs-mcp"],
"env": {
"PNLCS_URL": "https://example.com",
"PNLCS_IDENTIFIER": "your_identifier",
"PNLCS_SECRET": "your_secret"
}
}
}
}Client-by-client setup lives in mcp/README.md.
All UI strings live in the database (dynamic_translations table) and
flat PHP files under lang/<locale>/. Translations are editable from the
admin panel under Setup β Languages. Exporting
to JSON, importing, and AI-assisted batch translation are supported out
of the box.
If your language isn't covered yet, you can either submit a PR against the seeder or use the admin UI's export/import flow.
- All admin routes require authentication via the
admin.authmiddleware - Over 250 admin endpoints are gated by fine-grained permissions
- CSRF protection on every form
- Rate limiting on login, 2FA, password reset, and verification email resends
- Webhook routes are CSRF-exempt but validated by HMAC signatures
- Eloquent ORM everywhere (no raw SQL concatenation)
Please report security issues privately to security@panelica.com rather than opening a public GitHub issue.
PNLCS backs up its database automatically. The scheduled task
pnlcs:db-backup runs daily at 04:30 (through the cron runner from
installation step 13), dumps the database to a gzip file and rotates old ones.
- Where:
storage/app/backups/db/pnlcs-YYYYMMDD-His.sql.gz - Retention: the last 7 backups are kept (setting
db_backup_retention) - On/off: controlled by the
db_backup_enabledsetting (on by default)
Run one by hand:
php artisan pnlcs:db-backup # uses the defaults above
php artisan pnlcs:db-backup --dir=/mnt/backups --retention=30
php artisan pnlcs:db-backup --php # pure-PHP dump if mysqldump is missingIt uses mysqldump when present and falls back to a PHP dump with --php.
Restore a backup:
gunzip < storage/app/backups/db/pnlcs-20260101-043000.sql.gz | mysql -u pnlcs -p pnlcsThe scheduled job covers the database. Uploaded files (ticket attachments, invoice PDFs, branding) live under
storage/β include that directory in your server-level backup, and copy the.sql.gzfiles off the box (or point--dirat mounted/off-site storage) so a lost disk is not a lost backup.
Configure gateways under Setup β Payment Gateways. Open a gateway, fill in its keys, save, enable it, then test with a small order before going live. Checkout must run over HTTPS.
| Gateway | What you enter |
|---|---|
| Stripe | Publishable Key, Secret Key, Webhook Signing Secret |
| PayPal | PayPal Email, Client ID, Client Secret, Sandbox on/off |
| Mollie | Mollie API Key, Test Mode on/off |
| Razorpay | Key ID, Key Secret |
| Authorize.Net | API Login ID, Transaction Key |
| Tpay (Poland) | Open API Client ID + Secret, security code |
| Bank Transfer | Your bank/account details β shown to the client, confirmed by hand |
Card gateways confirm a payment by calling back, so set the webhook URL in the provider's dashboard to:
https://your-domain.com/gateway/<gateway>/webhook
for example β¦/gateway/stripe/webhook or β¦/gateway/paypal/webhook (also
paypal, authorize, mollie, razorpay, tpay). For Stripe, copy the
signing secret the dashboard shows for that endpoint into the gateway's
Webhook Signing Secret field β without it, incoming webhooks are rejected.
Bank Transfer has no webhook; you mark those invoices paid yourself.
500 β Composer detected issues in your platform: PHP >= 8.4.0
The site's PHP-FPM is older than the PHP that ran composer install. Because
the dependencies are locked against PHP 8.4 the page dies before Laravel even
boots, so storage/logs stays empty. Point the site (or FPM pool) at PHP
8.4 β on a control panel, change the domain's PHP version; on raw nginx, fix
the fastcgi_pass socket.
Application encryption key has not been specified / MissingAppKeyException
You skipped the key step. Run php artisan key:generate (it writes APP_KEY
into .env), then reload.
SQLSTATE⦠Base table or view not found
Migrations have not run, or .env points at the wrong database. Check the
DB_* values, make sure the database exists, then run
php artisan migrate --force.
Composer stops with β¦ does not exist and could not be created (writing to vendor/)
Something under vendor/ is owned by another user (usually a past
sudo composer), so the site user cannot write there. Fix ownership and retry:
chown -R <site-user>:<site-user> vendor && composer install --no-dev.
fatal: detected dubious ownership in repository
Git refuses a repo owned by a different user. Mark it safe:
git config --global --add safe.directory /path/to/pnlcs.
Blank or unstyled page, or the old UI after an update
The compiled assets or cached views are stale. Run npm run build, then
php artisan optimize (or php artisan view:clear).
.env is reachable in the browser
The web root points at the project folder instead of public/. Point the
document root at pnlcs/public β nothing above it should be web-served.
Invoices/emails never send, but nothing errors Three causes, in order of likelihood:
- Mail is not configured. Until SMTP is set under Setup β General
Settings β Mail Configuration,
MAIL_MAILER=logis in effect and nothing leaves the server. - The cron line is missing. With
QUEUE_CONNECTION=database, queued mail is sent by the scheduler (installation step 13), which starts a worker every minute. No cron, no worker: jobs stay in thejobstable. Check withcrontab -u www-data -l. - Mail is switched off in the panel settings β the log then says
Outgoing mail suppressed: mail is disabled in the panel settings.
The last step of the install wizard answers 500
storage/logs/laravel-*.log says file_put_contents(/var/www/pnlcs/.env): Failed to open stream: Permission denied. The wizard writes .env, and the
file belongs to root. Give it to the web user and reload the page:
sudo chown www-data:www-data .env && sudo chmod 640 .env (AlmaLinux/Rocky:
apache:apache). Current versions of the wizard show this on the requirements
page (".env β not writable") before you start.
AlmaLinux / Rocky: every page is a 500, and storage/logs is empty
SELinux is blocking PHP-FPM. sudo grep denied /var/log/audit/audit.log | tail
shows { write } β¦ name="logs" (cannot write storage/) and
{ name_connect } β¦ dest=3306 (cannot reach the database). Run the commands
in installation step 10.
AlmaLinux / Rocky: sudo: composer: command not found
sudo there does not search /usr/local/bin. Call Composer by its full path:
sudo -u apache /usr/local/bin/composer install --no-dev --optimize-autoloader.
git pull says detected dubious ownership, or npm fails with EACCES
Part of the tree belongs to root while the command runs as the web user
(usually an older install that ran sudo git clone / sudo npm install and
only handed storage/ over). Give the whole directory to the web user once:
sudo chown -R www-data:www-data /var/www/pnlcs, then re-run.
The server's IP address shows "Welcome to nginx!" instead of PNLCS
The distribution's default site is still enabled and answers every request
that does not match your server_name. Remove it:
sudo rm /etc/nginx/sites-enabled/default && sudo systemctl reload nginx.
apt installs no PHP at all on Debian 13 (Unable to locate package php8.4-imap)
Debian 13 does not package the imap extension; one missing package makes
apt refuse the whole line. Install the list without php8.4-imap β see
installation step 0.
Mailbox import does nothing, and the log says the PHP imap extension is not installed
Ticket import from a mailbox (Setup β Ticket Departments) needs PHP's
imap extension, which PHP 8.4 no longer bundles. Install php8.4-imap
(Ubuntu, ondrej PPA) or php-imap (AlmaLinux/Rocky, Remi) and reload PHP-FPM.
The rest of PNLCS does not need it.
A page shows Route [...] not defined right after an update
PHP-FPM is still serving the previous code from its opcode cache. Reload it:
sudo systemctl reload php8.4-fpm (AlmaLinux/Rocky: php-fpm).
The /install wizard is already locked, or you never got to choose a password
You ran php artisan db:seed by hand before opening the wizard; seeing an
administrator, it locks itself. Sign in with admin / admin123 and change
the password from your profile, or reinstall without seeding by hand.
How do I offer a free (zero-cost) plan?
Create the product and set Payment Type β Free (the dropdown offers
Recurring / One-time / Free). Do not put 0 in the price β that leaves the
product with no sellable price.
How do I bring an existing customer/account into PNLCS? Open the client β Services tab β Add Service. Leave the options empty for a billing-only record; use "Link to an existing account" to attach the account that already runs on the server (PNLCS then bills and manages it); or tick "Create the account on the server now" to provision a brand-new one.
How does PNLCS know which server account belongs to which service?
It stores the panel's internal account ID on the service (in module_data)
and every action β suspend, terminate, password β uses that ID. It does not
rely on usernames matching, so names can differ freely.
How do I update to the latest version?
Docker: docker exec pnlcs /usr/local/bin/update.sh. Self-hosted or inside a
panel account: see Updating. Your data is left untouched either
way.
Issues and pull requests are welcome.
Ways to help:
- Test a module against a real provider account β the module table shows which ones have only been tested with faked responses β and file an issue with what you found.
- Report a bug with reproduction steps β screenshots help a lot.
- Improve a translation via the admin UI's export, edit a JSON file, and send us a PR.
- Write a module β see Writing your own module.
- Write documentation β installation on other distributions or hosting panels, and anything this README gets wrong.
Please keep in mind:
- This is a side project for the Panelica team. We'll get to issues as quickly as we can but same-day responses are rare.
- PRs that include tests are prioritized.
- Please match the existing coding style (Laravel Pint + conventional commits).
Need help, want to report a bug, or have a feature request?
- π Bug reports & feature requests β GitHub Issues
- π¬ Community forum β forum.panelica.com
- π§ Email β info@panelica.com
- π Main site β panelica.com
- π Security disclosures β security@panelica.com (please do not open public issues for security)
We're happy to help from the forum or by email, but since this is a side project your patience is appreciated. For urgent matters, the forum tends to get the fastest community response.
- Panelica β the modern hosting control panel and a cPanel / Plesk alternative that needs no CloudLinux; its team builds and maintains PNLCS
- Laravel by Taylor Otwell and the Laravel community
- WHMCS β for inspiring much of the data model and workflow
- Every contributor who opens an issue or a pull request
Released under the MIT License. See LICENSE for details.
Keywords: WHMCS alternative Β· open-source hosting billing Β· self-hosted billing platform Β· Laravel billing Β· PHP client portal Β· hosting management software Β· free WHMCS Β· invoicing system Β· reseller hosting software Β· domain management Β· SSL management Β· support ticket system Β· hosting CRM Β· cPanel alternative Β· Plesk alternative Β· CyberPanel alternative Β· CloudLinux alternative Β· Panelica hosting control panel