Deployment guide

Paymenter

Overview

Paymenter is a free and open source billing and client management platform 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. It gives you an administration panel for products, pricing, currencies, coupons, taxes and recurring invoicing.

This SecureAnyCloud image delivers Paymenter fully installed on Ubuntu 24.04. Database migrations and seeded reference data are already in place, and there is no setup wizard to complete. You sign in and start building your catalogue. Payment gateways and server provisioning are handled by extensions, so orders can be paid through the processor you already use and handed to the system that creates the service.

What you get:

  • 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 and Redis 7.0, all from Ubuntu 24.04 main/universe. MariaDB holds the data, and Redis serves the cache, sessions and queue.
  • 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) and paymenter-schedule.timer (the scheduler, every minute) as enabled systemd units. They 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, so there is no default login to find.
  • Per-VM MariaDB password, Laravel APP_KEY and 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 SecureAnyCloud support

Prerequisites

You need an active Azure subscription, an SSH key pair, and a VNet with a subnet in the target region.

Standard_B2ms (2 vCPU / 8 GiB RAM) is a good starting size. Scale up as your catalogue, customer base and ticket volume grow.

Configure the network security group (NSG) to allow inbound 22/tcp from your management network. Also allow 443/tcp (HTTPS) and 80/tcp (which redirects to HTTPS) from your customers.

Step 1 - Deploy from the Azure Marketplace

In the Azure Marketplace, find Paymenter on Ubuntu 24.04 LTS by SecureAnyCloud and choose Get It Now, then Create.

Pick your resource group, region and VM size (Standard_B2ms or larger). Provide your SSH public key. In the networking step, allow inbound 443/tcp and 80/tcp. Then select Review + create.

Step 2 - Deploy with the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:paymenter:default:latest
az vm create \
  --resource-group <your-rg> \
  --name paymenter \
  --image sac1647283583153:paymenter:default:latest \
  --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

Connect over SSH as the azureuser account:

ssh azureuser@<vm-public-ip>

Step 4 - Confirm the services are running

Six units back Paymenter, and all should report active. The client area answers HTTP 200 over HTTPS. The administration panel 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 output: six lines of active, then client area /login -> HTTP 200, admin panel /admin -> HTTP 302 and plain http :80 -> HTTP 301. The 302 is the redirect to sign-in, which is what you want when nobody is logged in.

nginx listens on :80 and :443 on all interfaces. 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. It also generates the MariaDB password, the Laravel APP_KEY and the Passport OAuth signing keys, and writes them to a root-only file.

Check the file's permissions and view the non-secret entries:

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. It contains your sign-in address, the administrator email and password, and the MariaDB credentials. Copy them into your password manager, because nothing else on the VM can recover them.

You can verify that this administrator is genuinely per-VM, that the stored password is a bcrypt hash, and that no common default password works:

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 output: 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. The certificate is self-signed for this VM, so your browser shows a one-time warning. Accept it, or install your own certificate first (Step 10).

Sign in with the email address and password from Step 5.

Paymenter has a single sign-in for customers and staff. Your account holds the admin role, so 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, and each product carries one or more pricing plans. In the administration panel:

  • Go to Categories, then New category. Give it a name, a slug and a short description, for example Cloud Hosting.
  • Go to Products, then New product. Name it and pick the category you just created. 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. They 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 email from it. First boot sets both to this VM's address, and they must always agree. Compare them, and check the component versions:

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, then 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 you change only one of the two, the panel still loads, but generated links, password reset emails and payment redirects point at the old address.

Step 9 - The queue worker and the scheduler

Two background units make billing automatic. paymenter-queue.service is the Laravel queue worker. It processes jobs from Redis: sending mail, generating invoice PDFs, calling gateways and provisioning modules.

paymenter-schedule.timer fires schedule:run every minute. That run 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 output: 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. The scheduler's own heartbeat is under System > Cron Statistics.

If you change .env, restart the queue worker so it picks up the new configuration. 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 it is what makes each VM unique. It generates a certificate whose subject is this VM's own address. Inspect the service and the certificate:

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/sac/data/paymenter-firstboot.done

For production, replace the self-signed certificate with one for your domain. Point a DNS A record at the VM and 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/sac-paymenter
sudo sed -i 's|/etc/nginx/ssl/paymenter.key|/etc/letsencrypt/live/billing.example.com/privkey.pem|' /etc/nginx/sites-available/sac-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. Invoice and password reset emails 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=<set-at-first-boot> 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. 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 file 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 and unpack the storage tree. Then run php artisan cache:clear.

Step 13 - Updating Paymenter

The project publishes upgrades as new release tarballs, and you apply them with the built-in upgrade command. Take a backup first, then run:

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 shows 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, set both to your real address, and clear the cache.

A browser certificate warning on every visit. This is 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 emails 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, emails are written to that log file instead of being 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

To check the logs, run:

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 SecureAnyCloud with 24/7 support. Paymenter itself is an open source project. Its documentation is at paymenter.org/docs, and its source is at github.com/Paymenter/Paymenter under the MIT license.

Deploy on Azure

Launch Paymenter on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure