Paymenter on Ubuntu 24.04 on Azure User Guide
Overview
Paymenter is a free and open source billing and client management platform built for hosting providers and anyone selling recurring services. It gives your customers a public storefront, a checkout and a client area for their services, invoices and support tickets, and gives you an administration panel for products, pricing, currencies, coupons, taxes and recurring invoicing. Payment gateways and server provisioning are handled by extensions, so orders can be paid through the processor you already use and handed straight to the system that creates the service. The cloudimg image delivers Paymenter fully installed on Ubuntu 24.04 — a Laravel 12 application on PHP 8.3 with OPcache, served by nginx over HTTPS via php-fpm, with MariaDB for data and Redis for cache, sessions and the queue. Database migrations and the seeded reference data are already in place and the panel has no setup wizard to complete, so you sign in and start building your catalogue. Backed by 24/7 cloudimg support.
What is included:
- Paymenter 1.5.7 (MIT licensed) on Laravel 12.63.0, served from
/var/www/paymenter/public - nginx 1.24 (TLS on :443, with :80 redirected to HTTPS) + PHP 8.3.6 (php8.3-fpm with OPcache) + MariaDB 10.11 + Redis 7.0, all from Ubuntu 24.04 main/universe
- A per-VM self-signed TLS certificate generated on first boot, so the panel loads over a secure context from the very first request
paymenter-queue.service(the Laravel queue worker) andpaymenter-schedule.timer(the scheduler, every minute) as systemd units, both enabled — these are what send invoices, process renewals and act on overdue accounts- No user account of any kind in the shipped image. The first and only administrator is created on first boot with a random per-VM password — there is no default login to find
- Per-VM MariaDB password, Laravel
APP_KEYand Passport OAuth signing keys, all generated at first boot and written to a root-only file - Upstream telemetry shipped switched off, and no shared telemetry identity baked into the image
- 24/7 cloudimg support

Prerequisites
An active Azure subscription, an SSH key pair, and a VNet + subnet in the target region. Standard_B2ms (2 vCPU / 8 GiB RAM) is a good starting point; scale up as your catalogue, customer base and ticket volume grow. NSG inbound: allow 22/tcp from your management network and 443/tcp (HTTPS) plus 80/tcp (which redirects to HTTPS) from your customers.
Step 1 — Deploy from the Azure Marketplace
Find Paymenter on Ubuntu 24.04 LTS by cloudimg in the Azure Marketplace and choose Get It Now → Create. Pick your resource group, region, and VM size (Standard_B2ms or larger), provide your SSH public key, and allow inbound 443/tcp and 80/tcp in the networking step. Review and create.
Step 2 — Or deploy with the Azure CLI
az vm create \
--resource-group <your-rg> \
--name paymenter \
--image <marketplace-image-urn> \
--size Standard_B2ms \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_ed25519.pub \
--vnet-name <your-vnet> --subnet <your-subnet> \
--public-ip-sku Standard
az vm open-port --resource-group <your-rg> --name paymenter --port 443 --priority 1010
az vm open-port --resource-group <your-rg> --name paymenter --port 80 --priority 1020
Step 3 — Connect to your VM
ssh azureuser@<vm-public-ip>
Step 4 — Confirm the services are running
Six units back Paymenter and should all report active. The client area answers HTTP 200 over HTTPS, the administration panel correctly redirects an unauthenticated visitor to the sign-in page, and plain HTTP redirects to HTTPS:
systemctl is-active mariadb redis-server php8.3-fpm nginx paymenter-queue paymenter-schedule.timer
curl -ks -o /dev/null -w 'client area /login -> HTTP %{http_code}\n' https://127.0.0.1/login
curl -ks -o /dev/null -w 'admin panel /admin -> HTTP %{http_code}\n' https://127.0.0.1/admin
curl -s -o /dev/null -w 'plain http :80 -> HTTP %{http_code}\n' http://127.0.0.1/
Expected: six lines of active, then client area /login -> HTTP 200, admin panel /admin -> HTTP 302 (the redirect to sign-in, which is what you want when nobody is logged in) and plain http :80 -> HTTP 301.

Note which ports are published: nginx listens on :80 and :443 on all interfaces, while MariaDB (:3306) and Redis (:6379) are bound to loopback only and are never exposed to the network.
Step 5 — Retrieve your administrator credentials
The image ships with no accounts at all. On first boot, paymenter-firstboot.service creates the first and only administrator with a random password, generates the MariaDB password, the Laravel APP_KEY and the Passport OAuth signing keys, and writes them to a root-only file:
stat -c '%a %U:%G %n' /root/paymenter-credentials.txt
grep -E '^paymenter\.(url|admin\.url|admin\.user|db\.name|db\.user)=' /root/paymenter-credentials.txt
To see the passwords as well, read the whole file:
sudo cat /root/paymenter-credentials.txt
The file is mode 0600 root:root and contains your sign-in address, the administrator e-mail and password, and the MariaDB credentials. Copy them into your password manager — nothing else on the VM can recover them.

You can prove for yourself that this administrator is genuinely per-VM, that the stored password is a bcrypt hash, and that no common default password would have worked:
EMAIL="$(grep '^paymenter.admin.user=' /root/paymenter-credentials.txt | cut -d= -f2-)"
PASS="$(grep '^paymenter.admin.pass=' /root/paymenter-credentials.txt | cut -d= -f2-)"
HASH="$(mariadb -N -B paymenter -e "SELECT password FROM users WHERE email='${EMAIL}' LIMIT 1;")"
echo "accounts on this VM: $(mariadb -N -B paymenter -e 'SELECT count(*) FROM users;')"
echo "stored hash algorithm: $(echo "${HASH}" | cut -c1-4)"
HH="${HASH}" PP="${PASS}" php -r 'echo "your password verifies: ", password_verify(getenv("PP"),getenv("HH")) ? "yes" : "no", "\n";'
for g in password admin admin123 paymenter changeme; do
HH="${HASH}" GG="$g" php -r 'echo " default \"", getenv("GG"), "\" accepted: ", password_verify(getenv("GG"),getenv("HH")) ? "YES" : "no", "\n";'
done
Expected: accounts on this VM: 1, stored hash algorithm: $2y$, your password verifies: yes, and no for every guessed default.
Step 6 — Sign in
Open https://<vm-public-ip>/login in your browser. Because the certificate is self-signed for this VM, your browser shows a one-time warning — click through it, or install your own certificate first (Step 10). Sign in with the e-mail address and password from Step 5.

Paymenter has a single sign-in for customers and staff; because your account holds the admin role, the administration panel at https://<vm-public-ip>/admin opens once you are signed in. The dashboard shows live revenue, ticket and service figures and a revenue chart over your chosen window.

Step 7 — Build your first catalogue
Paymenter sells from a catalogue of categories and products, each product carrying one or more pricing plans. In the administration panel:
- Go to Categories → New category and give it a name, a slug and a short description — for example Cloud Hosting.
- Go to Products → New product, name it, pick the category you just created, then open the Pricing tab and add a plan. Choose
Recurring, a billing period (for example every 1 month) and a price in your currency. - Save. The product appears in the catalogue immediately.

Your customers see the same products on the public storefront under Shop, with the recurring price and an order button, and can register an account and check out from there.

Currencies, coupons, taxes and configurable options live under Configuration; payment gateways and server provisioning modules are installed from Extensions.
Step 8 — Understand where your public URL is stored
Paymenter keeps the customer facing address in two places: the APP_URL key in /var/www/paymenter/.env, and an app_url row in the settings database table. The database row is the authoritative one — Paymenter maps it onto Laravel's app.url config at runtime and generates every link, signed URL, asset path and invoice e-mail from it. First boot sets both to this VM's address, and they must always agree:
echo "settings.app_url (database) = $(/usr/local/sbin/paymenter-setting app_url)"
echo ".env APP_URL = $(grep -E '^APP_URL=' /var/www/paymenter/.env | cut -d= -f2-)"
grep -m1 "'version'" /var/www/paymenter/config/app.php
sudo -u www-data php /var/www/paymenter/artisan --version
php -v | head -1; mariadb --version; nginx -v

When you put Paymenter behind a real domain name, change both. Set the domain in the administration panel under Settings → General → App URL, and update .env to match:
sudo sed -i 's|^APP_URL=.*|APP_URL=https://billing.example.com|' /var/www/paymenter/.env
sudo -u www-data php /var/www/paymenter/artisan cache:clear
sudo -u www-data php /var/www/paymenter/artisan optimize:clear
sudo systemctl restart php8.3-fpm paymenter-queue
If only one of the two is changed, the panel will load but generated links, password reset e-mails and payment redirects will still point at the old address.
Step 9 — The queue worker and the scheduler
Two background units do the work that makes billing automatic. paymenter-queue.service is the Laravel queue worker that processes jobs from Redis — sending mail, generating invoice PDFs, calling gateways and provisioning modules. paymenter-schedule.timer fires schedule:run every minute, which is what raises invoices, sends reminders and suspends overdue services on schedule.
systemctl is-active paymenter-queue.service paymenter-schedule.timer
systemctl list-timers paymenter-schedule.timer --no-pager | head -3
echo "failed jobs: $(mariadb -N -B paymenter -e 'SELECT count(*) FROM failed_jobs;')"
Expected: active twice, a timer line showing the next run within the next minute, and failed jobs: 0. Failed jobs are also listed in the administration panel under Other → Failed Jobs, and the scheduler's own heartbeat under System → Cron Statistics.
If you change .env you must restart the queue worker so it picks the new configuration up — a long-running worker holds the old configuration in memory:
sudo systemctl restart paymenter-queue.service
Step 10 — First boot, and replacing the self-signed certificate
paymenter-firstboot.service runs once, before nginx and the queue worker, and is what makes each VM unique. It generates the certificate whose subject is this VM's own address:
systemctl show paymenter-firstboot.service -p Description,After,Before,ActiveState,Result
openssl x509 -in /etc/nginx/ssl/paymenter.crt -noout -subject -issuer -dates
ls -l /var/lib/cloudimg/paymenter-firstboot.done

For production, replace the self-signed certificate with one for your domain. Point a DNS A record at the VM, open 80/tcp and 443/tcp, then issue a certificate and repoint nginx at it:
sudo apt-get update && sudo apt-get install -y certbot
sudo certbot certonly --standalone -d billing.example.com --agree-tos -m admin@example.com --non-interactive
sudo sed -i 's|/etc/nginx/ssl/paymenter.crt|/etc/letsencrypt/live/billing.example.com/fullchain.pem|' /etc/nginx/sites-available/cloudimg-paymenter
sudo sed -i 's|/etc/nginx/ssl/paymenter.key|/etc/letsencrypt/live/billing.example.com/privkey.pem|' /etc/nginx/sites-available/cloudimg-paymenter
sudo nginx -t && sudo systemctl reload nginx
Then set the App URL to https://billing.example.com in both places as described in Step 8.
Step 11 — Mail, payments and telemetry
Out of the box MAIL_MAILER is log, so invoice and password reset e-mails are written to storage/logs/laravel.log instead of being delivered. Configure a real mailer under Settings → Mail in the administration panel, or in .env:
sudo -u www-data tee -a /var/www/paymenter/.env >/dev/null <<'ENV'
MAIL_MAILER=smtp
MAIL_HOST=smtp.example.com
MAIL_PORT=587
MAIL_USERNAME=billing@example.com
MAIL_PASSWORD=your-smtp-password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=billing@example.com
MAIL_FROM_NAME="Your Company"
ENV
sudo -u www-data php /var/www/paymenter/artisan config:clear
sudo systemctl restart paymenter-queue.service
Payment gateways (Stripe, PayPal, Mollie and others) and server provisioning modules are installed from Extensions in the administration panel and configured there — no shell access is required.
Upstream telemetry is shipped disabled in this image so nothing is reported without your consent, and no shared identifier is baked in. If you would like to support the project by sending anonymous usage data, remove the switch and let Paymenter generate an identity unique to your instance:
sudo sed -i '/^TELEMETRY_ENABLED=/d' /var/www/paymenter/.env
sudo -u www-data php /var/www/paymenter/artisan config:clear
Step 12 — Backups
Everything that matters lives in the MariaDB database and in /var/www/paymenter/storage (uploaded logos, product images and invoice PDFs). Check what you would be backing up:
du -sh /var/www/paymenter/storage
mariadb -N -B -e "SELECT CONCAT(table_schema,' = ',ROUND(SUM(data_length+index_length)/1024/1024,1),' MiB') FROM information_schema.tables WHERE table_schema='paymenter' GROUP BY table_schema;"
Take a consistent dump plus the storage tree:
sudo mariadb-dump --single-transaction --routines --events paymenter \
| gzip > /var/backups/paymenter-$(date +%F).sql.gz
sudo tar -czf /var/backups/paymenter-storage-$(date +%F).tar.gz -C /var/www/paymenter storage
Also keep a copy of /root/paymenter-credentials.txt and /var/www/paymenter/.env — the .env holds the APP_KEY, without which encrypted values in the database cannot be read back. To restore onto a fresh VM, put the same APP_KEY back into .env, load the dump, unpack the storage tree, then run php artisan cache:clear.
Step 13 — Updating Paymenter
Upgrades are published by the project as new release tarballs and applied with the built-in upgrade command. Take a backup first, then:
cd /var/www/paymenter
sudo -u www-data php artisan down
sudo -u www-data php artisan app:upgrade
sudo -u www-data php artisan migrate --force
sudo -u www-data php artisan cache:clear
sudo -u www-data php artisan up
sudo systemctl restart paymenter-queue.service
The administration panel also surfaces the available version under System → Updates.
Troubleshooting
The panel loads but links point at the wrong host. The two copies of the URL have drifted apart. Compare them with the commands in Step 8 and set both to your real address, then clear the cache.
A browser certificate warning on every visit. Expected until you install a certificate for your own domain — the shipped certificate is self-signed for this VM's IP address. See Step 10.
Invoices are not being raised, or e-mails never arrive. Check the two background units first: systemctl is-active paymenter-queue paymenter-schedule.timer. Then look at Other → Failed Jobs and System → Cron Statistics in the panel, and at /var/www/paymenter/storage/logs/laravel.log. If MAIL_MAILER is still log, e-mails are being written to that log file rather than sent — see Step 11.
A 500 error after editing .env. Laravel caches configuration; clear it and restart the worker:
sudo -u www-data php /var/www/paymenter/artisan optimize:clear
sudo systemctl restart php8.3-fpm paymenter-queue
Checking the logs:
journalctl -u paymenter-queue.service --no-pager -n 15
tail -n 15 /var/www/paymenter/storage/logs/laravel.log 2>/dev/null || echo "no application log yet"
tail -n 15 /var/log/nginx/error.log
Support
This image is built and maintained by cloudimg with 24/7 support. Paymenter itself is an open source project — its documentation lives at paymenter.org/docs and its source at github.com/Paymenter/Paymenter under the MIT licence.