"I'm an agency / consultant. I run PMA for multiple clients. Each client should see their own data, brand, URLs — but I'm the single operator + maintainer."
The agency pattern. Multi-env per client + per-client branding + per-client backups. Operationally one host, one git repo, one release flow.
Same multi-env mechanism as run-pma-as-a-cooperative — ASD_ENV namespaces every container, volume, port, tunnel. The difference is more around branding, customer-facing polish, and contracts:
ASD_ENV=client-<shortname>.pma.client-org.com) or sub-subdomain.ssh prod 'cd /opt/asd-pma && just bootstrap-local <profile> client-acme'
# Creates /opt/pma-client-acme/ with ASD_ENV=client-acme
Pick the profile per client need. A client running ticketing + chat needs support (Zammad + Mattermost + n8n). A client running full ops needs enterprise.
If the client has their own domain pointed at your host:
ssh prod 'cd /opt/pma-client-acme'
$EDITOR .env
# ASD_TUNNEL_HOST=pma.client-acme.com
# (or use a sub-subdomain pattern: redmine.acme.pma.your-host.com)
just net apply
# Regenerates Caddy routes with the new hostname
Add the wildcard DNS record (*.pma.client-acme.com → your host's IP) on the client's DNS, or use a tunnel server they point at.
Edit the client env's homepage:
$EDITOR /opt/pma-client-acme/packages/wikijs/scripts/content/pages/home.md
# Replace ASD-PMA branding with client-acme branding
# Replace "/asd" + "/pma" links with the services the client uses
# Add client logo as an image reference
Per-service customisation if relevant:
| Service | Where to customise |
|---|---|
| Mattermost | Admin UI → Site Configuration → Customization (logo, sitename) |
| Redmine | Admin UI → Settings → Display (custom logo + footer text) |
| Wiki.js | Admin UI → Theming (logo, colors, custom CSS) |
| Authentik | Admin UI → Customization → Brands (per-realm branding for login page) |
For deeper branding, fork the PMA repo per client and commit branding changes to that fork. Pull upstream PMA changes periodically.
Edit /opt/pma-client-<name>/services.yaml (or per-service manifest.yaml):
backup:
enabled: true
type: database
retention:
daily: 30 # keep 30 days of daily backups
weekly: 12 # plus 12 weekly snapshots
monthly: 12 # plus 12 monthly snapshots
Schedule the backups via cron on the host:
# /etc/cron.d/pma-backups
0 2 * * * pma cd /opt/pma-client-acme && just backup && asd data push
0 3 * * * pma cd /opt/pma-client-bravo && just backup && asd data push
0 4 * * * pma cd /opt/pma-client-charlie && just backup && asd data push
asd data push (if configured) uploads the backup snapshots to off-host storage — important for client-facing setups where "we lost their host" should still leave a recovery path.
If a client wants their own admin access (without you holding their password):
Same code, different envs. Release a fix once per client:
# For all clients sequentially
for env in client-acme client-bravo client-charlie; do
ssh prod "cd /opt/pma-${env} && just release-run 1234"
done
Or wrap in a release-all.sh script with error handling + summary.
Take a final off-host backup, then destroy the env:
# Take a final backup
ssh prod 'cd /opt/pma-client-acme && just backup && asd data push'
# Hand over the backup to the client (rsync, S3 link, whatever)
# Tear down
ssh prod 'cd /opt/asd-pma && just env destroy client-acme --force'
# Removes containers, volumes, Caddy routes, tunnel registry,
# and /opt/pma-client-acme/ directory.
One host. N clients. Each in their own /opt/pma-client-*/ directory with their own containers, data, URLs, backups, branding. You release once per client; they each see only their own data.
ssh prod 'ls /opt/ | grep ^pma-client-'
pma-client-acme
pma-client-bravo
pma-client-charlie
ssh prod 'docker ps --format "{{.Names}}" | grep -oP "^asd-client-[a-z]+" | sort -u'
asd-client-acme
asd-client-bravo
asd-client-charlie
Common patterns:
PMA's licensing cost is zero — your charge is for hosting + operations + customisation. Margins are typically much better than SaaS reseller models.
ASD_CLIENT_ID per client so the URLs are partitioned cleanly. Alternative: share one client-id but use per-client subdomain prefixes.asd-client-acme-redmine-postgres). DO NOT consolidate to a single Postgres to "save resources" — you lose the data-isolation property that makes the agency pattern safe./pma/learn/07-deploy-to-prod covers bootstrap-local <profile> <env>.run-pma-as-a-cooperative.