"PMA doesn't bundle service X. I want it managed by the same framework — manifest, SSO, backups, release flow."
The mechanical recipe. For the deeper "why manifests work this way" walk, see /pma/learn/02-add-a-service.
Adding a service is dropping a directory under packages/. The framework finds it on next just package install <name> or next bootstrap. Three minimum files, three optional ones.
We'll use Vaultwarden (a self-hosted Bitwarden-compatible password manager) as the worked example. The pattern transfers to any service.
Before writing the manifest, find:
vaultwarden/server:1.30.5)80)DOMAIN, ADMIN_TOKEN, SMTP_* optional)SSO_ENABLED=true)/data)The service's GitHub README + Docker Hub page have these. 5-10 minutes of reading.
mkdir -p packages/vaultwarden/scripts
packages/vaultwarden/manifest.yamlname: vaultwarden
display_name: Vaultwarden
category: security
classification: optional
priority: 90
image:
name: vaultwarden/server
tag: '1.30.5'
env_var: VAULTWARDEN_IMAGE_TAG
containers:
- name: vaultwarden
env_var: VAULTWARDEN_CONTAINER_NAME
role: app
required: true
health_check: true
container_user: 0 # vaultwarden runs as root inside the container
requirements:
ports: 1
internal_port: 80
port_env:
- env: VAULTWARDEN_PORT
default: 8222
health:
endpoint: /alive
expected_code: 200
sso:
type: oidc
redirect_path: /identity/connect/oidc-signin
configured: false
backup:
enabled: true
type: volume
volumes:
- suffix: vaultwarden-data
data_uid: 0
framework:
profiles:
- enterprise
- full
startup_priority: 90
abbreviation: VW
tags:
- security
- passwords
installation:
status: available
packages/vaultwarden/net.manifest.yamlid: "${{ macro.concat('env:ENV_PREFIX', 'vaultwarden') }}"
name: Vaultwarden
seedDefaultRegistry: true
endpoint:
url: "http://127.0.0.1:${{ env.VAULTWARDEN_PORT }}"
caddy:
publishPreferred: true
hostRoute:
host: "${{ macro.concat('env:ENV_PREFIX', 'vaultwarden') }}-${{ macro.tunnelClientId() }}.${{ macro.tunnelEndpoint() }}"
tunnel:
public: true
env:
VAULTWARDEN_URL: "${{ macro.exposedOrigin() }}"
packages/vaultwarden/tpl.envVAULTWARDEN_PORT=8222
VAULTWARDEN_IMAGE_TAG=1.30.5
VAULTWARDEN_ADMIN_TOKEN=changeme-generate-with-openssl-rand
DOMAIN=${VAULTWARDEN_URL}
SIGNUPS_ALLOWED=false
SSO_ENABLED=true
SSO_AUTHORITY=${AUTHENTIK_URL}/application/o/vaultwarden/
SSO_CLIENT_ID=vaultwarden
packages/vaultwarden/docker-compose.yamlservices:
vaultwarden:
image: vaultwarden/server:${VAULTWARDEN_IMAGE_TAG}
container_name: ${VAULTWARDEN_CONTAINER_NAME}
ports:
- "${VAULTWARDEN_PORT}:80"
volumes:
- vaultwarden-data:/data
environment:
DOMAIN: ${DOMAIN}
ADMIN_TOKEN: ${VAULTWARDEN_ADMIN_TOKEN}
SIGNUPS_ALLOWED: ${SIGNUPS_ALLOWED}
SSO_ENABLED: ${SSO_ENABLED}
SSO_AUTHORITY: ${SSO_AUTHORITY}
SSO_CLIENT_ID: ${SSO_CLIENT_ID}
SSO_CLIENT_SECRET: ${VAULTWARDEN_SSO_CLIENT_SECRET}
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost/alive"]
interval: 30s
restart: unless-stopped
volumes:
vaultwarden-data:
just package install vaultwarden
Framework reads manifest → creates Authentik OAuth app → writes VAULTWARDEN_SSO_CLIENT_SECRET to .env → starts container → adds Caddy route → brings tunnel up.
$ just status vaultwarden
[asd-dev-vaultwarden] ✓ healthy
$ grep ^VAULTWARDEN_URL= .env
VAULTWARDEN_URL=https://vaultwarden-xyz1.eu2.tn.example.com
$ curl -sI ${VAULTWARDEN_URL}/alive | head -1
HTTP/2 200
Open the URL → Vaultwarden login → "Use single sign-on" → Authentik handles auth → land back authenticated.
| Service type | backup.type |
Notes |
|---|---|---|
| Postgres / MySQL / MariaDB-backed | database |
Standard pg_dump / mysqldump flow |
| SQLite / file-backed (Vaultwarden, n8n) | volume or workspace |
Tar the data dir |
| Stateless reverse-proxy or worker | config |
Snapshot the .env slice |
| Multi-volume service (Mattermost: db + files) | database + volumes array |
Both get backed up |
| Service type | sso.type |
Notes |
|---|---|---|
| Has OAuth2 plugin / strategy | oauth |
Most common — works with Authentik OAuth provider |
| Speaks OIDC natively (modern apps) | oidc |
Slightly cleaner than oauth for newer services |
| Only has SAML | saml |
Authentik can serve as SAML IdP |
| No native SSO support | proxy |
Authentik runs as forward-auth in front of the service |
/pma/learn/02-add-a-service.containers: in the manifest + multiple services in the compose file./pma/reference/manifest (planned) — every manifest field documented.