"Multi-tenant pattern. One PMA per organisation / team / department / client, with isolation but operationally shared (one host, one operator, one release flow)."
Two patterns. Pick by isolation requirements.
"Multi-tenant" can mean two very different things:
| Pattern | Isolation | Operator cost | When to use |
|---|---|---|---|
| Pattern A — Multi-env on one host | Strong (separate containers, volumes, DBs, tunnels) | Low (one host, one git repo, one release flow) | Cooperatives, agencies serving multiple clients, dept-per-env in one company |
| Pattern B — One PMA, partition by Authentik group + Redmine project | Logical only (same DB, same containers) | Lowest (one of everything) | Small team sub-projects, "we're all in this together" coops |
Pattern A is what most cooperatives reach for. We'll walk that one in detail.
One env name per tenant. Short, lowercase, no spaces.
ASD_ENV=coop-a # client A
ASD_ENV=coop-b # client B
ASD_ENV=coop-c # client C
Each becomes a directory under /opt/pma-<env>/ with its own .env, its own containers (named asd-coop-a-redmine, asd-coop-b-redmine, ...), its own volumes, its own ports, its own tunnel subdomains.
# Tenant A
ssh prod 'cd /opt/asd-pma && just bootstrap-local enterprise coop-a'
# Creates /opt/pma-coop-a/, bootstraps in there.
# Tenant B
ssh prod 'cd /opt/asd-pma && just bootstrap-local enterprise coop-b'
# Tenant C
ssh prod 'cd /opt/asd-pma && just bootstrap-local enterprise coop-c'
The enterprise profile can be the same; the <env> argument is what isolates them.
After each bootstrap, the tenant's services are live at:
https://redmine-<coop-a-client-id>.<tunnel>
https://redmine-<coop-b-client-id>.<tunnel>
https://redmine-<coop-c-client-id>.<tunnel>
(Different ASD_CLIENT_ID per tenant if you use separate tunnel-server accounts; same tunnel server is fine.)
Each tenant has its own release flow:
# Release to tenant A
ssh prod 'cd /opt/pma-coop-a && just release-run 1234'
# Release to tenant B
ssh prod 'cd /opt/pma-coop-b && just release-run 1234'
You can release the same ticket to all tenants sequentially (or with parallel if you want simultaneous deploys).
Each /opt/pma-<env>/ is a checkout you can customise:
coop-a uses enterprise, coop-b uses support (no ERPNext)./opt/pma-<env>/packages/wikijs/scripts/content/pages/home.md for tenant-specific landing.tpl.env defaults: per-tenant SMTP server, per-tenant company name.Common framework code stays shared. Per-tenant config diverges.
Add a tenant:
ssh prod 'cd /opt/asd-pma && just bootstrap-local <profile> <new-env>'
Remove a tenant (when they leave the coop):
ssh prod 'cd /opt/asd-pma && just env destroy <env-name> --force'
# Stops containers, removes volumes, removes Caddy routes,
# removes tunnel registry entries, removes /opt/pma-<env>/ dir.
Per just env destroy, the cleanup is total — backups too. Take a final backup OFF the host before destroying.
ssh prod 'docker ps --format "{{.Names}}" | grep -E "^asd-coop-" | sort | head'
asd-coop-a-redmine
asd-coop-a-mattermost
asd-coop-a-n8n
...
asd-coop-b-redmine
asd-coop-b-mattermost
...
asd-coop-c-redmine
...
Three independent PMA installs. One host. One operator. Three Authentik instances (one per tenant — each tenant manages its own users). Three sets of public URLs.
Backups taken per-tenant via per-env just backup (run inside each /opt/pma-<env>/). Cron job per tenant if you want scheduled backups.
If tenants share one team and the isolation is mostly "don't see each other's tickets":
group-coop-a, group-coop-b).Pattern B is cheaper but leaks: tenants share the same DB, same containers, same release schedule. One tenant's data corruption affects everyone. Choose A unless the cost difference matters.
ASD_CLIENT_ID (otherwise subdomains collide). Generate via asd token create per tenant./opt/pma-<env>/.asd/workspace/backups/. If you push them off-host (recommended), include the env name in the destination path so they don't collide.release-run for each env + aggregates results./pma/learn/07-deploy-to-prod — the bootstrap-local <profile> <env> pattern.