Files
wild-directory/ADDING-APPS.md
Paul Payne 4d983819c9 feat(supabase): add services and statefulset for database management
feat(synapse): update ingress to use traefik ingress class and bump version

feat(syncthing-discovery): introduce syncthing discovery service with deployment and ingress

feat(syncthing-relay): add syncthing relay server with deployment and ingress configuration

fix(taiga): update liveness and readiness probes to use tcpSocket for health checks

fix(taiga): change PVC access mode to ReadWriteMany for media and static storage

feat(traefik): add icon and ignore rules for traefik service

docs(ushahidi): add notes for Redis configuration and Laravel startup probe adjustments

feat(ushahidi): implement dedicated Redis deployment for Ushahidi

fix(vllm): update deployment strategy and readiness/liveness probes for improved stability

fix(writefreely): pin writefreely image version to v0.15.1 for consistency

docs(zulip): add notes for TLS-terminating reverse proxy configuration and expected behavior
2026-07-02 21:34:27 +00:00

701 lines
23 KiB
Markdown

# Adding Wild Cloud Apps
For contributors and maintainers creating or modifying Wild Cloud app packages. For using existing apps, see [README.md](README.md).
## Directory Structure
Each app has a two-level structure: an `app.yaml` meta file at the root, and version-specific files inside `versions/`. Name version directories by **slot** (typically the major version), not the full version string. The actual version lives in `manifest.yaml` inside the slot.
```
myapp/
├── app.yaml # App identity, latest slot pointer, upgrade routing
├── notes.md # App-specific operational notes (optional but encouraged)
└── versions/
├── 2/ # Current latest slot (manifest.yaml has version: 2.3.1)
│ ├── manifest.yaml # Version-specific config (requires, defaultConfig, etc.)
│ ├── kustomization.yaml
│ └── *.yaml # Kubernetes resource templates
└── 1/ # Waypoint slot (only if upgrade routing needs it)
├── manifest.yaml
├── kustomization.yaml
└── *.yaml
```
Most apps have **one** version directory. Add a second only when a waypoint is needed for upgrade routing.
## Required Files
Every app directory must contain:
1. **`app.yaml`** — App identity, latest slot pointer, and upgrade routing rules
2. **`versions/{slot}/manifest.yaml`** — Version-specific configuration schema
3. **`versions/{slot}/kustomization.yaml`** — Kustomize configuration with Wild Cloud labels
4. **`versions/{slot}/*.yaml`** — Kubernetes resource templates
## App Meta (`app.yaml`)
```yaml
name: immich
is: immich
description: Immich is a self-hosted photo and video backup solution that allows you to store, manage, and share your media files securely.
icon: https://immich.app/assets/images/logo.png
latest: "1"
```
| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | App identifier — must match directory name |
| `is` | Yes | Unique id used for `requires` mapping |
| `description` | Yes | Brief description shown in listings |
| `icon` | Yes | URL to app icon for UI display |
| `category` | Yes | Category for filtering in the App Directory |
| `latest` | Yes | Slot name — directory name under `versions/` (not a version string) |
| `upgrade` | No | Upgrade routing rules (see Upgrade Metadata below) |
### Categories
Use an established category when one fits. Introduce a new one only when an app clearly doesn't belong anywhere existing.
| Category | Use for |
|----------|---------|
| `services` | Wild Cloud infrastructure (traefik, metallb, cert-manager, etc.) |
| `database` | Database engines (postgres, mysql, redis, memcached) |
| `productivity` | Wikis, project management, file sharing, collaboration |
| `business` | ERP, CRM, accounting, forms, marketing automation |
| `communication` | Chat, messaging, video conferencing, email |
| `social` | Social networks and fediverse apps |
| `community` | Community organizing, civic tech, decision-making |
| `education` | Learning management systems |
| `developer` | Dev tools, code hosting, package management |
| `security` | VPN, password management |
| `ai` | AI/ML inference and interfaces |
| `media` | Photo and video management |
## Version Manifest (`versions/{slot}/manifest.yaml`)
```yaml
version: 1.135.3-1
requires:
- name: pg
alias: db # Use a different reference name in templates
- name: redis # 'alias' and 'installedAs' default to 'name' value
defaultConfig:
namespace: immich
externalDnsDomain: "{{ .cloud.domain }}"
storage: 250Gi
cacheStorage: 10Gi
domain: immich.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
db:
host: "{{ .apps.pg.host }}"
name: immich
user: immich
redis:
host: "{{ .apps.redis.host }}"
defaultSecrets:
- key: password
- key: dbUrl
default: "postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?pool=30"
requiredSecrets:
- db.password
- redis.auth
```
| Field | Required | Description |
|-------|----------|-------------|
| `version` | Yes | App version (see Versioning Convention below) |
| `requires` | No | Dependency apps with optional aliases |
| `defaultConfig` | Yes | Default configuration merged into the operator's `config.yaml` |
| `defaultSecrets` | No | This app's secrets — no `default` value means auto-generated |
| `requiredSecrets` | No | Secrets from dependency apps — format: `<app-ref>.<key>` `[WC-DOT]` |
### Versioning Convention
Use the two-part scheme `<upstream>-<revision>`:
- **Upstream version** tracks the third-party software (e.g., `1.120.2`, `v4.0.18`)
- **Packaging revision** tracks Wild Cloud packaging changes with no upstream change
**Examples:**
- `v4.0.18` — initial packaging
- `v4.0.18-1` — first packaging fix, no upstream change
- `v4.0.19` — upstream version bump, revision resets
**Bump the packaging revision** for: manifest field changes, template improvements, security context fixes, label corrections, etc.
**Bump the upstream version** when updating the container image tag.
### Slot Naming Convention
- Use the **major version** as the slot name (`1`, `2`, `v3`)
- Keep the `v` prefix if upstream uses it (`v1` for cert-manager)
- **Never** put packaging revisions (`-1`, `-2`) in directory names
- **Never** put minor/patch versions in directory names unless you need a distinct waypoint
| App | Slot name | Version in manifest |
|-----|-----------|-------------------|
| Ghost 5.118.1-2 | `5` | `5.118.1-2` |
| cert-manager v1.17.2 | `v1` | `v1.17.2` |
| Immich 1.135.3-1 | `1` | `1.135.3-1` |
When bumping versions, update files inside the existing slot. Only create a new directory when you need a waypoint.
### Upgrade Metadata
The `upgrade` field is **optional** — omit it for apps where any version can upgrade directly to any other. That covers 90% of apps.
Add `upgrade:` only when an app has breaking changes that need a controlled path (e.g., Discourse requires stepping through major versions).
#### The `upgrade` block in `app.yaml`
```yaml
name: myapp
latest: "3"
upgrade:
from:
- version: ">=3.5.0"
- version: ">=3.4.0"
via: "2" # Must pass through slot "2" first
- version: "<3.4.0"
blocked: true
notes: "Requires sequential major upgrades. See upstream docs."
preUpgrade:
backup: required # "none", "recommended", or "required"
```
`latest` and `via` are **slot names** (directory names), not version strings.
| Field | Description |
|-------|-------------|
| `from[].version` | Version constraint: `>=`, `>`, `<=`, `<`, `=`, or `>0` |
| `from[].via` | Waypoint slot to pass through first |
| `from[].blocked` | Block upgrade with an error message |
| `from[].notes` | Message shown when blocked or as context |
| `preUpgrade.backup` | `"required"` blocks until backup is done; `"recommended"` warns |
Version-specific migration behavior goes in the version's `manifest.yaml`:
```yaml
# versions/3/manifest.yaml
version: 3.6.0
upgrade:
migrations:
pre:
- migrations/pre-deploy.yaml
post:
- migrations/post-deploy.yaml
configMigrations:
oldKeyName: newKeyName
```
| Field | Description |
|-------|-------------|
| `migrations.pre` | K8s Job YAMLs to run before deploying this version |
| `migrations.post` | K8s Job YAMLs to run after deploying |
| `configMigrations` | Map of old config key → new config key for automatic renaming |
#### Waypoint versions
When an upgrade requires an intermediate step, add the waypoint as a new slot alongside `latest`:
```
myapp/
├── app.yaml # latest: "3", routing rules
└── versions/
├── 3/ # Latest (version: 3.6.0)
└── 2/ # Waypoint (version: 2.8.0)
```
To create a waypoint: leave the current latest slot in place and create a new directory for the new major version. Update `app.yaml` to point `latest` at the new slot and add routing rules.
#### Migration jobs
Migration jobs are K8s Job manifests. Write them **idempotent** — safe to re-run if an upgrade is retried.
Place migration files in the version slot and reference them from `manifest.yaml`:
```yaml
# versions/3/migrations/db-migrate.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: myapp-db-migrate
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: migrate
image: myapp:3.6.0
command: ["bundle", "exec", "rake", "db:migrate"]
```
- **`pre`** — schema changes the new code needs before it starts
- **`post`** — backfills or cleanup after the new version is running
#### Config key renames
Use `configMigrations` to automatically rename config keys during upgrade:
```yaml
# versions/2/manifest.yaml
version: 2.1.0
upgrade:
configMigrations:
dbHost: db.host
dbPort: db.port
```
### Adding a New Version
#### Simple version bump (no breaking changes)
Update files inside the existing slot. Do not rename directories or change `app.yaml`.
```bash
vi wild-directory/myapp/versions/1/manifest.yaml # bump version
vi wild-directory/myapp/versions/1/deployment.yaml # update image tag
wild app add myapp && wild app deploy myapp
```
#### Breaking changes (waypoint required)
```bash
mkdir -p wild-directory/myapp/versions/3
# add manifest.yaml, kustomization.yaml, *.yaml for the new version
# update app.yaml: set latest: "3", add upgrade routing rules with via: "2"
```
#### Schema migrations
Add migration job files to the slot directory and reference them from `manifest.yaml`:
```yaml
# versions/2/manifest.yaml
version: 2.1.0
upgrade:
migrations:
pre:
- migrations/pre-deploy.yaml
```
Migration jobs must be idempotent. Use `CREATE IF NOT EXISTS`, `ALTER TABLE IF NOT EXISTS`, etc.
### Dependencies
```yaml
requires:
- name: postgres
alias: db
- name: redis
```
- `name` — the app to depend on (any installed app with a matching `is` field satisfies it)
- `alias` — reference name in templates (defaults to `name`)
At installation time, the operator maps each dependency to an actual installed app instance. The system tracks this mapping via `installedAs` in the local manifest.
### Template Variables
All template variables referenced in resource files must be defined in `defaultConfig`.
#### Variable namespaces
1. `{{ .cloud.* }}`, `{{ .cluster.* }}`, `{{ .operator.* }}` — standard Wild Cloud variables
2. `{{ .app.* }}` — this app's config
3. `{{ .apps.<ref>.* }}` — dependency app config, resolved via alias or name
4. `{{ .secrets.* }}` — this app's secrets (in `defaultSecrets` only)
#### Available configuration variables
##### operator
- `operator.email`
##### cloud
- `cloud.domain`, `cloud.baseDomain`, `cloud.internalDomain`
- `cloud.dnsmasq.ip`, `cloud.dnsmasq.interface`
- `cloud.router.ip`, `cloud.router.dynamicDns`, `cloud.dhcpRange`
- `cloud.nfs.host`, `cloud.nfs.mediaPath`, `cloud.nfs.storageCapacity`
- `cloud.dockerRegistryHost`
- `cloud.backup.root`
##### cluster
- `cluster.name`, `cluster.hostnamePrefix`
- `cluster.nodes.talos.version`, `cluster.nodes.talos.schematicId`
- `cluster.nodes.control.vip`
- `cluster.ipAddressPool`, `cluster.loadBalancerIp`
- `cluster.certManager.cloudflare.domain`, `cluster.certManager.cloudflare.zoneID`
- `cluster.externalDns.ownerId`
- `cluster.dockerRegistry.storage`
##### apps
Each deployed app gets a section under `apps.<name>.*`. Common fields:
- `apps.<name>.namespace`, `domain`, `externalDnsDomain`, `tlsSecretName`
- `apps.<name>.db.host`, `db.port`, `db.name`, `db.user`
- `apps.<name>.redis.host`
- `apps.<name>.smtp.host`, `smtp.port`, `smtp.user`, `smtp.from`, `smtp.tls`, `smtp.startTls`
#### Dependency reference resolution
`{{ .apps.<ref>.* }}` resolves by checking `alias` first, then `name`. The system uses the `installedAs` value to find the actual app config.
### Kustomization (`kustomization.yaml`)
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: immich
labels:
- includeSelectors: true
pairs:
app: immich
managedBy: kustomize
partOf: wild-cloud
resources:
- deployment-server.yaml
- ingress.yaml
- namespace.yaml
- pvc.yaml
- service.yaml
- db-init-job.yaml
```
- Set `namespace` to the app name `[WC-NS]`
- Include standard Wild Cloud labels with `includeSelectors: true` `[WC-SEL]`
- List every resource file under `resources:` `[WC-RES]`
`includeSelectors: true` automatically injects the standard labels into resource selectors. Individual resources can use simple component labels (`component: web`) — Kustomize expands them.
**Do not use Helm-style labels** (`app.kubernetes.io/name`, etc.). Use `component: web`, `component: worker`, etc. `[WC-HELM]`
## Configuration Templates
Resource files are gomplate templates compiled when operators add apps. Only variables defined in `defaultConfig` are available to resource templates.
### Ingress
Use this as the standard ingress template:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp
annotations:
external-dns.alpha.kubernetes.io/target: {{ .externalDnsDomain }}
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
external-dns.alpha.kubernetes.io/ttl: "60"
spec:
ingressClassName: traefik
rules:
- host: {{ .domain }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp
port:
number: 80
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}
```
- Always use `spec.ingressClassName: traefik` — do not use the deprecated `kubernetes.io/ingress.class` annotation `[WC-ING-ANN]` `[WC-ING-CLS]`
- Do not add `cert-manager.io/cluster-issuer` — the wildcard TLS cert is pre-distributed to namespaces; this annotation overwrites it with a wrong per-app cert `[WC-CERT]`
- Do not add Traefik redirect annotations — HTTPS redirect is handled globally
- Define `externalDnsDomain`, `domain`, and `tlsSecretName` in `defaultConfig`
The `external-dns.alpha.kubernetes.io/target` annotation creates a CNAME from the app subdomain to the cluster domain. Always set `cloudflare-proxied: "false"` since Wild Cloud manages its own TLS. `[WC-DNS]`
## Database Setup
### Database Initialization Jobs
Include a `db-init-job.yaml` for every app that uses a database. The job must:
- Create the application database if it doesn't exist
- Create/update the application user with the correct credentials
- Grant permissions
- Install any required database extensions (`vector`, `bloom`, `pg_trgm`, etc.)
- Use `restartPolicy: OnFailure`
- Be listed in `kustomization.yaml` resources
- Use the appropriate security context (`runAsUser: 999` for PostgreSQL)
- Be **idempotent** — safe to re-run after a redeploy
See `immich`, `gitea`, or `openproject` for reference implementations.
### Database URLs
When apps need a database URL with embedded credentials, use a dedicated `dbUrl` secret — do not construct URLs in-place:
```yaml
# Wrong: Kustomize cannot do runtime env var substitution
- name: DB_URL
value: "postgresql://user:$(DB_PASSWORD)@host/db"
# Correct: use a secret with the full URL
- name: DB_URL
valueFrom:
secretKeyRef:
name: myapp-secrets
key: dbUrl
```
Add `dbUrl` to `defaultSecrets` with the URL template as the `default` value.
### Database Env Var Naming
Name database-related env vars so the backup system can identify them:
- **Database name**: include `DATABASE`, `DB_NAME`, `DBNAME`, or `__DATABASE` in the env var name
- **Database URLs**: the value must contain `://`
- **Usernames**: include `USER` in the name — these are not patched even if the value matches the DB name
```yaml
- name: DB_NAME # patched on restore
value: myapp
- name: DB_USER # not patched (contains USER)
value: myapp
- name: DATABASE_URL # patched (value contains ://)
value: "postgresql://myapp:secret@postgres/myapp"
```
## Deployment Strategy `[WC-RWO]`
Set `strategy: type: Recreate` on any Deployment that uses a `ReadWriteOnce` PVC. RWO volumes attach to one pod at a time — `RollingUpdate` causes Multi-Attach errors when the new pod tries to mount while the old one still holds the volume.
```yaml
spec:
replicas: 1
strategy:
type: Recreate
```
## Security Contexts `[WC-SC-POD]` `[WC-SC-CTR]`
Set security contexts at both pod and container levels on all workloads:
```yaml
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
seccompProfile:
type: RuntimeDefault
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
```
Common UIDs: PostgreSQL `999`, Redis `999`. Consult the image docs for others.
For images that must run as root (s6-overlay, linuxserver.io), omit `runAsNonRoot` from the pod context and suppress `[WC-SC-POD]` / `[WC-SC-CTR]` with `ignoreRules` if the upstream manifest doesn't support it.
## Secrets
Define app secrets in `defaultSecrets`. Reference secrets from dependencies in `requiredSecrets`.
```yaml
defaultSecrets:
- key: dbPassword
- key: apiKey
requiredSecrets:
- db.password # from postgres (aliased as 'db')
- redis.auth
```
Reference them in resources as:
```yaml
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-secrets
key: dbPassword # own secret
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-secrets
key: db.password # required secret
```
Never hardcode secret values in templates. Never commit `secrets.yaml`.
## Converting Helm Charts
When an official Helm chart exists, convert it rather than writing manifests from scratch.
```bash
helm fetch --untar --untardir charts repo/chart-name
helm template --output-dir base --namespace myapp --values values.yaml myapp charts/chart-name
```
Then convert to Wild Cloud format:
- Replace hardcoded values with gomplate variables (`{{ .cloud.domain }}`, etc.)
- Replace Helm labels with Wild Cloud standard labels
- Add `includeSelectors: true` to kustomization
- Use simple component labels (`component: web`, not `app.kubernetes.io/name`)
- Add security contexts
- Add external-dns annotations to ingresses
- Update secrets to use dotted-path convention
**Helm style → Wild Cloud style:**
```yaml
# Remove:
labels:
app.kubernetes.io/name: myapp
app.kubernetes.io/instance: release-name
# Add to kustomization.yaml:
labels:
- includeSelectors: true
pairs:
app: myapp
managedBy: kustomize
partOf: wild-cloud
# In individual resources:
labels:
component: server
```
## Post-Deploy Scripts
Some apps require a management command after first deploy to create an admin account, register a node, or invite the first user. Package these as shell scripts in `scripts/` alongside the kustomize files.
Register every script in `manifest.yaml` — the web UI shows a parameter form for each one:
```yaml
scripts:
- name: create-superuser
path: scripts/create-superuser.sh
description: "Create the initial admin account."
params:
- name: EMAIL
required: true
- name: PASSWORD
description: Leave blank to generate a random one
```
Follow `synapse/versions/v1/scripts/create-user.sh` as the reference implementation. See [docs/scripts.md](docs/scripts.md) for the full conventions and common commands.
## Common Gotchas
Keep app-specific operational notes in a `notes.md` file inside the app directory.
### Image availability
Before packaging, confirm the image pulls without authentication (`docker pull <image>`). Bitnami images moved off Docker Hub; ghcr.io images often require auth even for public repos. If no public image exists, add `ignoreRules: [WC-IMG]` with a comment.
### After template changes
After editing any file in wild-directory, re-run `wild app add <app>` before deploying — compiled templates in the instance directory are not auto-updated.
### PostgreSQL
If using PostgreSQL, see [docs/database.md](docs/database.md) — `sslmode=disable`, db-init job pattern, URL construction, env var naming.
### MySQL
If using MySQL, see [docs/database.md](docs/database.md) — db-init idempotency (`CREATE USER` + `ALTER USER`), required secret references.
### Redis
If using Redis, see [docs/redis.md](docs/redis.md) — authenticated URL pattern, apps that don't support passwords.
### Runtime memory, startup delays, health probes
If the app uses Node.js, Python, PHP, Ruby, JVM, nginx, or linuxserver.io images, see [docs/](docs/).
### Hex-format secrets
If an app requires a hex secret (e.g. exactly 64 chars for Outline), derive one with `crypto.SHA256`:
```yaml
defaultSecrets:
- key: utilsSecret
- key: secretKey
default: '{{ crypto.SHA256 .secrets.utilsSecret }}'
```
Referenced secrets must appear earlier in the list.
### nginx proxy DNS `[WC-DKRDNS]`
If the app uses an nginx proxy config from Docker, see [docs/nginx.md](docs/nginx.md) — `127.0.0.11` doesn't exist in Kubernetes.
### Traefik "No available server" `[WC-SEL]`
If Traefik returns "No available server" after a redeploy, see [docs/traefik.md](docs/traefik.md) — immutable selector fix.
### Same-origin iframes
If the app embeds its own sub-pages in iframes and they break, see [docs/traefik.md](docs/traefik.md) — `X-Frame-Options` interaction.
## Validation
Run the validation script before submitting any new or modified app:
```bash
# From the wild-directory root:
admin/scripts/validate-app.sh <app-name>
# Or from inside your app directory:
../../admin/scripts/validate-app.sh
```
**Errors must be fixed before submitting.** Warnings are strong suggestions — fix them unless you have a specific documented reason not to.
For the full list of rule IDs, severities, and fix instructions:
```bash
admin/scripts/validate-apps.py --list-rules
```
**Suppressing rules:**
If your app has a legitimate reason to deviate (e.g. packaging upstream Helm-rendered manifests), add `ignoreRules` to `app.yaml` with a comment:
```yaml
ignoreRules:
- WC-HELM # upstream Helm-rendered manifests
- WC-SEL # upstream manifests; selectors are set by the Helm chart
```
Only suppress rules when genuinely necessary.
## Submission Checklist
Run the validator first — it catches most issues automatically:
```bash
admin/scripts/validate-app.sh <app-name>
```
**Errors must be fixed. Warnings must be fixed or suppressed with a comment in `ignoreRules`.**
Then verify the things the validator can't check:
- [ ] Docker images pull without authentication (`docker pull <image>`)
- [ ] App deploys and runs correctly in the `test-cloud` instance
- [ ] Dependencies (database, redis, etc.) work correctly end-to-end
## Contributing
1. Fork the repository
2. Create a new app directory following the structure above
3. Test your app in `test-cloud`
4. Submit a pull request with a description of the app and any special notes
## Notice: Third-Party Software
The Kubernetes manifests and Kustomize files in this directory are designed to deploy **third-party software**.
Unless otherwise stated, the software deployed by these manifests **is not authored or maintained** by this project. All copyrights, licenses, and responsibilities for that software remain with the respective upstream authors.
These files are provided solely for convenience and automation. Users are responsible for reviewing and complying with the licenses of the software they deploy.
This project is licensed under the GNU AGPLv3 or later, but this license does **not apply** to the third-party software being deployed.