Compare commits

...

8 Commits

Author SHA1 Message Date
Paul Payne
763ac922d5 Add synapse federation Ingress for serverName path routing 2026-07-10 06:12:32 +00:00
Paul Payne
1b3200a5f1 Idempotent db-init passwords. 2026-07-10 05:14:24 +00:00
Paul Payne
dc5518b57f Externalize networking (welcome Wild Central!) 2026-07-10 03:49:38 +00:00
Paul Payne
bb5a76b864 Crowsec uses external LAPI. 2026-07-10 03:37:56 +00:00
Paul Payne
219a28bc65 Fixes synapse. 2026-07-10 03:36:15 +00:00
Paul Payne
387838819b Adds wild-directory server. 2026-07-10 03:35:56 +00:00
Paul Payne
c80d8dc411 Adds plausible. 2026-07-10 03:35:41 +00:00
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
281 changed files with 4698 additions and 2556 deletions

View File

@@ -1,362 +0,0 @@
# Adding Apps - Notes & Documentation Improvements
## Documentation Improvement Candidates
Issues and gaps discovered while adding apps. These should be folded back into ADDING-APPS.md.
### 1. Node.js app memory requirements
Node.js apps (NocoDB, Outline, etc.) need more than 512Mi memory. NocoDB crashed with OOM at 512Mi.
- Recommended: 1Gi limit, 512Mi request for Node.js apps
- Set `NODE_OPTIONS=--max-old-space-size=768` to keep heap within the limit
- **Doc suggestion**: Add a section in ADDING-APPS.md about runtime-specific resource defaults:
- Node.js apps: 1Gi limit, 512Mi request; add NODE_OPTIONS env var
- JVM apps (Java/Kotlin): 1Gi+ limit
- Python/Ruby apps: 512Mi typically sufficient
- Go/Rust apps: 256-512Mi typically sufficient
### 2. db-init-job is repeated boilerplate
The db-init-job.yaml for PostgreSQL apps is nearly identical across all apps. Only the app name, namespace, and secret name change.
- **Doc suggestion**: Provide a copy-paste template for the standard PostgreSQL db-init-job with placeholders marked clearly.
### 4. Apps requiring hex-encoded secrets
Some apps (Outline) require secrets in specific formats (e.g., exactly 64 hex chars). The default Wild Cloud `GenerateSecret` produces alphanumeric (not hex) strings.
- **Solution**: Use `crypto.SHA256` gomplate function in the manifest `default` field to generate a valid 64-char hex string from another secret:
```yaml
defaultSecrets:
- key: utilsSecret
- key: secretKey
default: '{{ crypto.SHA256 .secrets.utilsSecret }}'
```
- **Doc suggestion**: Add this pattern to ADDING-APPS.md with examples of apps that need it. Note that the order of `defaultSecrets` matters — referenced secrets must come first.
### 6. Always use `?sslmode=disable` in PostgreSQL connection strings
The Wild Cloud internal PostgreSQL does not have SSL enabled. Apps that construct PostgreSQL connection strings must include `?sslmode=disable`, and deployments should set `PGSSLMODE=disable`. Without it, apps crash with "The server does not support SSL connections."
- Already done correctly in: listmonk, gitea, discourse
- Missing from: outline (fixed), any new app
- **Doc suggestion**: Add to the ADDING-APPS.md dbUrl template examples: always append `?sslmode=disable` to postgres connection strings.
### 5. Re-running `wild app add` is required after template changes
When you modify files in wild-directory (e.g., fix a template), you must re-run `wild app add <app>` before deploying. The compiled templates in the instance `apps/` directory are NOT automatically updated when wild-directory changes. Only then will `wild app deploy` use the updated templates.
- **Doc suggestion**: Add to ADDING-APPS.md: "After modifying templates in wild-directory, always re-run `wild app add <app>` to recompile before deploying."
### 7. WriteFreely requires root user (jrasanen/writefreely image)
The `jrasanen/writefreely` image startup script creates `config.ini` in `/writefreely/` which is a root-owned directory. The image's default user is non-root, causing "Permission denied" errors at startup.
- **Fix**: Set `runAsUser: 0` and `runAsNonRoot: false` in the pod securityContext
- Still safe with `allowPrivilegeEscalation: false` and `capabilities.drop: ALL`
- **Doc suggestion**: Note that some community images require root. When an image fails with permission denied on startup, check if it needs root and add the security context override with capabilities dropped.
### 8. Redis URL must include password for authenticated Redis
Wild Cloud's Redis requires authentication. Apps that take a Redis URL must include the password in the URL: `redis://:password@host:6379`.
- **Pattern**: Add `redis.password` to `requiredSecrets`, then use K8s env var expansion:
```yaml
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-secrets
key: redis.password
- name: REDIS_URL
value: redis://:$(REDIS_PASSWORD)@{{ .redis.host }}:6379
```
- The `$(VAR_NAME)` syntax is supported by Kubernetes in container env values (evaluated at pod start)
- **Doc suggestion**: Add this pattern to ADDING-APPS.md under "Redis Authentication" section.
### 3. Consistent health check paths across apps
Different apps use different health check paths (`/health`, `/healthz`, `/_health`, `/api/v1/health`). This should be documented per app type or noted that you must verify the actual path from the app's Docker docs.
- For apps where no valid HTTP health path exists (e.g., Typebot viewer which only serves chatbot URLs), use `tcpSocket` probe instead of `httpGet`.
### 9. Linuxserver.io and s6-overlay images require root + capabilities
Images using s6-overlay init system (most linuxserver.io images) must run as root and need full capabilities to switch to their internal "abc" user.
- Set `runAsUser: 0, runAsNonRoot: false` in pod securityContext
- Do NOT set `allowPrivilegeEscalation: false` or `capabilities.drop: ALL` in container securityContext
- Only keep `readOnlyRootFilesystem: false`
- Same applies to Nextcloud (uses rsync/chown during setup)
- **Affects**: BookStack (linuxserver.io), any linuxserver.io image
### 10. CryptPad needs emptyDir + initContainer for config directory
The CryptPad image's `/cryptpad/config/` directory is in the image overlay filesystem and not writable by the container process even when running as root.
- **Fix**: Mount an emptyDir at `/cryptpad/config`, and use an initContainer to pre-seed `config.example.js` from the image into the emptyDir.
- The initContainer mounts the emptyDir at a different path (e.g., `/config-dest`) and copies the file.
- **CPAD_CONF** env var must be set to `/cryptpad/config/config.js` (startup script copies config.example.js to config.js if config.js doesn't exist).
### 11. Pixelfed image requires authentication (ghcr.io access restriction)
~~`ghcr.io/pixelfed/pixelfed` returns 403 Forbidden for anonymous pulls.~~
- **Resolved**: Use `ghcr.io/mattlqx/docker-pixelfed:v0.12.7-nginx` (community image, publicly accessible)
- Note: the mattlqx image uses port 80 (nginx), not 8080. Update `containerPort` and service `targetPort` accordingly.
### 12. Bitnami images no longer on Docker Hub
Bitnami moved their images from Docker Hub (`bitnami/appname`) to their own OCI registry. Apps that were packaged using Bitnami images (e.g., Ghost) need to be updated to use the official upstream image or Bitnami's new registry.
- **Ghost fix**: Switched from `bitnami/ghost:5.x` to official `ghost:5.130.6-alpine`
- The official ghost image uses different env vars (`database__connection__host` instead of `GHOST_DATABASE_HOST`) and a different mount path (`/var/lib/ghost/content` instead of `/bitnami/ghost`)
- Check image availability before packaging: `docker manifest inspect docker.io/bitnami/appname:tag`
### 13. Required secrets belong in app-secrets, not dep-secrets
Apps that use `requiredSecrets` (e.g., `mysql.rootPassword`) receive those secrets copied into their own `<app>-secrets` K8s Secret, under the key `<dep>.<key>` (e.g., `mysql.rootPassword`). db-init jobs and deployments must reference `<app>-secrets`, NOT `mysql-secrets`.
```yaml
# WRONG - mysql-secrets doesn't exist in the app namespace
secretKeyRef:
name: mysql-secrets
key: rootPassword
# CORRECT - the required secret is copied into ghost-secrets
secretKeyRef:
name: ghost-secrets
key: mysql.rootPassword
```
### 15. mattlqx/docker-pixelfed requires APP_PORT env var
The `ghcr.io/mattlqx/docker-pixelfed` community image generates nginx config from env vars using `sed`. The `listen` directive uses `APP_PORT`, which must be set explicitly or nginx crashes with "invalid number of arguments in 'listen' directive".
- Add `APP_PORT: "80"` to the deployment env vars
- The image listens on port 80 (nginx), not 8080 as the original pixelfed image did
- Both the web and worker deployments must mount the shared storage PVC — use ReadWriteMany access mode, not ReadWriteOnce, since both pods need it simultaneously
### 14. Your Priorities and Polis have no public Docker images
- `ghcr.io/citizensfoundation/your-priorities-app` — 403 Forbidden, images require auth
- `compdemocracy/polis-server` — private, no public Docker Hub or ghcr.io images
- Both apps require building custom images from source before they can be packaged
- Attempting to add these to wild-directory results in ImagePullBackOff
### 16. PHP/Laravel and other heavy-framework apps need extended probe delays
Laravel's startup process runs package discovery, autoload optimization, and encryption key generation before the HTTP server is ready. This commonly takes 23 minutes on cluster restart or first boot.
- The default `initialDelaySeconds: 60` is too short — the liveness probe fires before the app is ready, kills the container, and a restart loop begins
- Set `initialDelaySeconds: 120` and `failureThreshold: 6` for liveness probes on Laravel/Symfony apps
- Set `initialDelaySeconds: 60` for readiness probes
- The symptom is: pod shows `Running` for ~2 minutes then gets `Killing` due to liveness probe failure, followed by a new pod starting the same cycle
- The same applies to other frameworks with heavy startup initialization: Rails (`bundle exec`, asset precompilation), Spring Boot (JVM + Spring context loading), Django with migrations
- **Affects**: Ushahidi API (Laravel) — fixed with 120s initial delay + failureThreshold: 6
### 17. MySQL db-init `CREATE USER IF NOT EXISTS` doesn't update passwords
The common db-init pattern `CREATE USER IF NOT EXISTS 'user'@'%' IDENTIFIED BY '${PASSWORD}'` only sets the password at creation time. If the MySQL user already exists from a previous deployment (e.g., after `wild app delete` + `wild app add` without dropping the database), the password is silently left unchanged and the new deployment fails with "Access denied for user".
- **Symptom**: App starts, connects to MySQL, gets "Access denied" even though the db-init job completed successfully
- **Quick fix**: Exec into the MySQL pod and run: `ALTER USER 'user'@'%' IDENTIFIED BY 'current_password'; FLUSH PRIVILEGES;`
- **Better pattern** — make db-init jobs truly idempotent for both user creation and password:
```sql
CREATE USER IF NOT EXISTS '${DB_USERNAME}'@'%' IDENTIFIED BY '${DB_PASSWORD}';
ALTER USER '${DB_USERNAME}'@'%' IDENTIFIED BY '${DB_PASSWORD}';
GRANT ALL PRIVILEGES ON ${DB_DATABASE_NAME}.* TO '${DB_USERNAME}'@'%';
FLUSH PRIVILEGES;
```
- This `CREATE ... IF NOT EXISTS` + `ALTER USER` pattern is safe to run repeatedly regardless of whether the user existed before
- **Affects**: Ghost (MySQL backend) — hit this when redeploying after a previous partial test run
### 18. Gancio "Non empty db" crash on re-deploy
Gancio stores its setup state in `config.json` on the data PVC. If the PVC is lost or the app is deleted and redeployed with an existing database, gancio finds a non-empty DB but no `config.json` and refuses to start with "Non empty db! Please move your current db elsewhere than retry."
- **Fix**: Drop the database schema and let gancio recreate it:
```bash
kubectl exec -n postgres <postgres-pod> -- psql -U postgres gancio \
-c "DROP SCHEMA public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO gancio; GRANT ALL ON SCHEMA public TO public;"
kubectl scale deployment -n gancio gancio --replicas=0
# (wait for pod to terminate)
kubectl scale deployment -n gancio gancio --replicas=1
```
- Scale down first to release the DB connection before dropping the schema
- This only affects re-deployments; fresh installs work fine
### 19. Odoo requires explicit database name and `-i base` on first run
The official Odoo Docker image does not auto-initialize the database. Without specifying the database name and installing the base module, Odoo starts in multi-database manager mode and health checks fail with "Database not initialized".
- Add `args: ["-d", "DATABASE_NAME", "-i", "base"]` to the container spec
- The `-i base` flag installs Odoo's base module and creates all database tables on first run
- Subsequent runs with `-i base` are safe (idempotent) — it updates rather than reinstalls
- Use a `startupProbe` with `failureThreshold: 40, periodSeconds: 30` (20 minutes) since first-run initialization takes 1015 minutes
- Once startup probe passes, the regular liveness/readiness probes with `initialDelaySeconds: 0` take over
### 20. Headscale v0.29+ CLI: inconsistent user flag types
In headscale v0.29, different commands accept different user identifiers:
- `preauthkeys create --user <id>` — requires numeric ID
- `auth register --user <username>` — accepts username string
- `users destroy --identifier <id>` — requires numeric ID, not a positional arg
```bash
# Always list users first to get the ID
headscale users list
headscale preauthkeys create --user 1 # numeric ID
headscale auth register --auth-id <id> --user payne # username string OK
headscale users destroy --identifier 1 # numeric ID
```
Node registration flow: when a Tailscale client connects via browser (not pre-auth key), headscale
shows a command like `headscale auth register --auth-id hskey-authreq-XXXX --user USERNAME`.
Run it via `kubectl exec -n headscale deploy/headscale -- headscale auth register --auth-id <id> --user <username>`.
### 21. "No available server" from Traefik — kustomize label selector mismatch
If a deployment was originally created without kustomize labels in its selector (or with stale templates), re-deploying via `wild app deploy` cannot fix it because `spec.selector` on a Deployment is immutable once set. The Service selector will update (it's mutable) but pods won't match it, leaving the Service with no endpoints.
- **Symptom**: Traefik returns "No available server" even though pods are Running
- **Diagnosis**: `kubectl get endpoints <app> -n <namespace>` shows `<none>`; pod labels don't include `app: <name>` / `managedBy: kustomize` / `partOf: wild-cloud`
- **Fix**: Delete the affected deployments and re-apply kustomize — `kubectl delete deployment <name> -n <ns>` then `kubectl apply -k <instance>/apps/<app>/`
- **Root cause**: The Deployment selector is set correctly on first deploy, but if the resource already existed with only `component: web` (e.g., from a pre-kustomize deployment), the selector is stuck
### 22. Apps using same-origin iframes need SAMEORIGIN frame policy
The global Traefik `security-headers` middleware applies `X-Frame-Options: SAMEORIGIN` to all responses (changed from `DENY`). Apps that use iframes internally (like Etherpad's pad editor, which loads `../static/empty.html` in an iframe) were broken by the stricter `DENY` policy.
- **Root cause**: `DENY` prevents even same-origin iframes. `SAMEORIGIN` still blocks cross-origin embedding while allowing apps to embed their own sub-pages.
- **Symptoms**: JavaScript error `Blocked a frame with origin "https://..." from accessing a cross-origin frame` — occurs when an app creates a same-origin iframe and then the `X-Frame-Options: DENY` header causes the browser to block access to it.
- **Important**: Route-level Traefik middlewares run BEFORE entrypoint middlewares on the response path. A per-app middleware cannot override a global entrypoint middleware — the entrypoint middleware always runs last.
- **Fix applied globally**: Changed `frameDeny: true` to `X-Frame-Options: SAMEORIGIN` in `customResponseHeaders` in the crowdsec `security-headers` middleware.
- **Affects**: Etherpad (pad editor iframe), any app that loads sub-pages in iframes on the same domain.
### 24. nginx proxy containers: Docker DNS resolver (`127.0.0.11`) doesn't work in Kubernetes
Apps that use nginx as a frontend proxy (e.g., karrot-frontend) often ship with an nginx config template that uses Docker's embedded DNS resolver:
```nginx
resolver 127.0.0.11 valid=3s;
set $backend app-backend:8000;
proxy_pass http://$backend$request_uri;
```
This fails in Kubernetes with `send() failed (111: Connection refused) while resolving, resolver: 127.0.0.11`. There's a second trap: **any nginx variable anywhere in the `proxy_pass` URL** (even `$request_uri` in the path) forces runtime DNS resolution and requires a `resolver` directive. Removing the resolver line but keeping `proxy_pass http://backend:8000$request_uri;` still fails with `no resolver defined to resolve backend`.
**Fix**: override the nginx config template via a ConfigMap mounted with `subPath`, and remove the variable from `proxy_pass` entirely:
```nginx
# Instead of:
resolver 127.0.0.11 valid=3s;
set $backend ${BACKEND};
proxy_pass http://$backend$request_uri;
# Use:
proxy_pass http://${BACKEND};
```
With no nginx variable in `proxy_pass`, nginx resolves the hostname at startup using the pod's `/etc/resolv.conf` (which points to CoreDNS). The full request URI is still forwarded automatically in a regex `location ~` block. Mount the ConfigMap with `subPath` to override just the template file without replacing the whole directory.
### 25. ConfigMap changes don't restart pods — and `subPath` mounts never auto-update
When you update a ConfigMap and run `wild app deploy` (`kubectl apply`), the ConfigMap object is updated but **pods are not restarted**. You must explicitly restart them:
```bash
kubectl rollout restart deployment/<name> -n <namespace>
```
There's a further subtlety: ConfigMaps mounted with `subPath` are **never** live-updated in the pod filesystem, even if you wait. The pod must be restarted to pick up changes regardless of the `subPath` mount update behavior.
This means: after any ConfigMap change, always follow up with a rollout restart for affected deployments.
### 26. The `includeSelectors: true` mismatch affects every deployment in the app — check them all
When diagnosing "no available server" from Traefik (note #21), it's easy to focus only on the deployment that serves the HTTP route and miss other deployments in the same namespace. The `includeSelectors: true` label propagation affects every resource in the kustomization — if any deployment pre-existed with only `component: X` labels, its Service will have `<none>` endpoints and dependent services (backends, workers, Redis) will silently fail.
**Always run this after any selector-related redeploy**:
```bash
kubectl get endpoints --all-namespaces | grep "<none>"
```
Check every deployment in the affected namespace, not just the one Traefik is routing to. A Redis pod with `<none>` endpoints will cause the backend to crash with `ConnectionError` even though the Redis pod itself shows `1/1 Running`.
**Checklist when fixing a selector mismatch in an app**:
1. `kubectl get endpoints -n <app>` — identify all services with `<none>`
2. `kubectl get pods -n <app> --show-labels` — confirm which pods have old labels
3. Delete ALL affected deployments (not just the web one), then redeploy
4. Verify all endpoints are populated before declaring the app fixed
### 23. Post-deploy admin scripts for apps that require manual first-run steps
Some apps can't complete setup through the web UI until an initial admin account is created via a CLI command. Package these as shell scripts in `scripts/` alongside the kustomize files so users have a repeatable, documented way to run them.
**When to add a script**: If the app's README or upstream docs require running a management command after deploy (creating a superuser, registering a node, inviting the first user, etc.) — wrap it in a script.
**Always register scripts in `manifest.yaml`** — the web UI automatically shows a button with a parameter form for every script listed there. Without this, users have to drop to the CLI even though the UI fully supports it. Format:
```yaml
scripts:
- name: create-superuser
path: scripts/create-superuser.sh
description: "Short description shown in the UI."
params:
- name: EMAIL
description: Description shown in the param form
required: true
- name: PASSWORD
description: Leave blank to generate a random one
```
**Script conventions** (follow `synapse/versions/v1/scripts/create-user.sh` as the reference):
- Require `KUBECONFIG`, `WILD_INSTANCE`, and `WILD_API_DATA_DIR` env vars; exit with a clear error if missing
- Read `namespace` from `config.yaml` via `yq` so the script doesn't hardcode values
- Auto-generate passwords with `openssl rand` if `PASSWORD` is not supplied
- Find the running pod by label (don't hardcode a pod name); fail clearly if none found
- Print credentials at the end with a "save this — it won't be shown again" warning
- Reference the script in the app README's "First-Time Setup" section in place of a raw `kubectl exec` command
**Non-interactive Django `createsuperuser`**:
```bash
kubectl exec -n <ns> <pod> -c <container> -- \
env DJANGO_SUPERUSER_PASSWORD="${PASSWORD}" \
python manage.py createsuperuser --email "${EMAIL}" --noinput
```
The `--noinput` flag reads the password from `DJANGO_SUPERUSER_PASSWORD`.
**Affected apps so far**: Eventyay (`scripts/create-superuser.sh`), Synapse (`scripts/create-user.sh`), Headscale (`scripts/register-node.sh`).
## App Status Tracking
| App | Status | Notes |
|-----|--------|-------|
| Firefly III | ✅ working | |
| Wiki.js | ✅ working | |
| Outline | ✅ working | Redis URL needs password via K8s env var expansion |
| WriteFreely | ✅ working | Needs runAsUser: 0 (community image quirk) |
| NocoDB | ✅ working | Node.js needs 2Gi memory |
| Mattermost | ✅ working | |
| Etherpad | ✅ working | |
| CryptPad | ✅ working | Needs emptyDir + initContainer for config seeding |
| MediaWiki | ✅ working | Post-deploy web installer required for LocalSettings.php |
| Nextcloud | ✅ working | |
| PeerTube | ✅ working | |
| BookStack | ✅ working | linuxserver.io needs root + no capabilities.drop |
| Typebot | ✅ working | Builder: /signin health path; Viewer: tcpSocket probe |
| Pixelfed | ✅ working | Use ghcr.io/mattlqx/docker-pixelfed:v0.12.7-nginx (community), port 80 not 8080 |
| Wekan | pending | needs MongoDB |
| Mautic | ✅ working | |
| Taiga | ✅ working | |
| Pol.is | ❌ blocked | Private AWS ECR images, no public alternative |
| Zulip | ✅ working | Bundles own postgres/rabbitmq/memcached; first-run migrations take ~10min |
| Rocket.Chat | pending | needs MongoDB |
| LimeSurvey | ✅ working | |
| Gancio | ✅ working | Re-deploy: must drop DB schema first if prior data exists (see note 18) |
| Mobilizon | ✅ working | |
| Formbricks | ✅ working | |
| Jitsi | ✅ working | |
| Mastodon | already done | |
| Chamilo | ✅ working | |
| Moodle | ✅ working | |
| Open edX | pending | complex |
| Pretix | ✅ working | |
| Indico | ✅ working | |
| Eventyay | ✅ working | No versioned Docker tags upstream — main tag is intentional |
| Leihs | pending | |
| LibreBooking | ✅ working | |
| uMap | ✅ working | Host header needed in health probes (Django ALLOWED_HOSTS) |
| MapComplete | pending | |
| Terrastories | pending | |
| Karrot | ✅ working | nginx frontend: override template via ConfigMap to remove Docker DNS resolver (see note 24) |
| LiquidFeedback | pending | |
| Your Priorities | ❌ blocked | No public Docker images (ghcr.io requires auth) |
| Helios Voting | pending | |
| Belenios | pending | |
| Decidim | already done | |
| Alaveteli | pending | |
| FixMyStreet | pending | |
| Ghost | ✅ working | Bitnami image gone; use official ghost:alpine; different env vars + mount path (see note 12) |
| Ushahidi | ✅ working | Client: runAsUser:0 + CHOWN/SETUID/SETGID; API: liveness initialDelay 120s (Laravel slow start) |
| CiviCRM | pending | |
| Open Collective | pending | |
| Open Food Network | pending | |
| Resonate | pending | |
| BookWyrm | ✅ working | |
| Lemmy | already done | |
| Akaunting | ✅ working | |
| GnuCash | skip | desktop app, not containerizable |
| OhMyForm | ✅ working | |
| Odoo | ✅ working | Needs `-d DATABASE -i base` args; uses startupProbe (20min window) for first-run DB init |
| Headscale | ✅ working | SQLite, ConfigMap-based config, no secrets; v0.29 CLI uses numeric user IDs (see note 20) |
| Baserow | ✅ working | |
| Keila | already done | |
| Listmonk | already done | |
| Discourse | already done | |
| Gitea | already done | |
| Immich | already done | |
| Loomio | already done | |
| Synapse | already done | App renamed from matrix to synapse |
| Open WebUI | already done | |
| OpenProject | already done | |
| vLLM | already done | |

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
- @README.md
- ADDING-APPS.md is the expert advice you need when adding new apps to the Wild Cloud Directory.
- docs/ contains deep-dive guides — runtime-specific (nodejs, python, php, ruby, jvm, nginx, linuxserver) and topic-specific (database, redis, traefik, scripts).
## Finding good sources of documentation for adding a new app to the Wild Cloud Directory

View File

@@ -3,10 +3,6 @@ kind: Ingress
metadata:
name: akaunting
namespace: akaunting
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:
@@ -20,7 +16,3 @@ spec:
name: akaunting
port:
number: 80
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}

View File

@@ -1,12 +1,10 @@
version: 3.1.21-1
version: 3.1.21-2
requires:
- name: mysql
- name: smtp
- name: mysql
- name: smtp
defaultConfig:
namespace: akaunting
externalDnsDomain: '{{ .cloud.domain }}'
domain: akaunting.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
storage: 2Gi
locale: en-US
dbPrefix: aka_
@@ -15,7 +13,7 @@ defaultConfig:
adminEmail: '{{ .operator.email }}'
db:
host: '{{ .apps.mysql.host }}'
port: "3306"
port: '3306'
name: akaunting
user: akaunting
smtp:
@@ -24,8 +22,8 @@ defaultConfig:
from: '{{ .apps.smtp.from }}'
user: '{{ .apps.smtp.user }}'
defaultSecrets:
- key: dbPassword
- key: adminPassword
- key: smtpPassword
- key: dbPassword
- key: adminPassword
- key: smtpPassword
requiredSecrets:
- mysql.rootPassword
- mysql.rootPassword

View File

@@ -2,10 +2,6 @@ apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: aptly
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:
@@ -19,7 +15,3 @@ spec:
name: aptly
port:
number: 80
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}

View File

@@ -1,11 +1,9 @@
version: 1.6.2-3
version: 1.6.2-4
defaultConfig:
namespace: aptly
externalDnsDomain: "{{ .cloud.domain }}"
domain: "aptly.{{ .cloud.domain }}"
tlsSecretName: wildcard-wild-cloud-tls
domain: aptly.{{ .cloud.domain }}
storage: 5Gi
apiUser: aptly
operatorEmail: "{{ .operator.email }}"
operatorEmail: '{{ .operator.email }}'
defaultSecrets:
- key: apiPassword
- key: apiPassword

View File

@@ -69,6 +69,15 @@ spec:
volumeMounts:
- name: baserow-data
mountPath: /baserow/data
securityContext:
runAsNonRoot: false
runAsUser: 0
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
seccompProfile:
type: RuntimeDefault
livenessProbe:
httpGet:
path: /api/_health/

View File

@@ -3,10 +3,6 @@ kind: Ingress
metadata:
name: baserow
namespace: {{ .namespace }}
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:
@@ -20,7 +16,3 @@ spec:
name: baserow
port:
number: 80
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}

View File

@@ -1,23 +1,21 @@
version: 2.2.2-1
version: 2.2.2-3
requires:
- name: postgres
- name: redis
- name: postgres
- name: redis
defaultConfig:
namespace: baserow
externalDnsDomain: "{{ .cloud.domain }}"
domain: baserow.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
storage: 2Gi
db:
host: "{{ .apps.postgres.host }}"
port: "5432"
host: '{{ .apps.postgres.host }}'
port: '5432'
name: baserow
user: baserow
redis:
host: "{{ .apps.redis.host }}"
host: '{{ .apps.redis.host }}'
defaultSecrets:
- key: dbPassword
- key: secretKey
- key: dbPassword
- key: secretKey
requiredSecrets:
- postgres.password
- redis.password
- postgres.password
- redis.password

View File

@@ -3,10 +3,6 @@ kind: Ingress
metadata:
name: bookstack
namespace: {{ .namespace }}
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:
@@ -20,7 +16,3 @@ spec:
name: bookstack
port:
number: 80
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}

View File

@@ -1,12 +1,10 @@
version: 26.05.1-2
version: 26.05.1-3
requires:
- name: mysql
- name: smtp
- name: mysql
- name: smtp
defaultConfig:
namespace: bookstack
externalDnsDomain: '{{ .cloud.domain }}'
domain: bookstack.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
storage: 2Gi
db:
host: '{{ .apps.mysql.host }}'
@@ -19,8 +17,8 @@ defaultConfig:
from: '{{ .apps.smtp.from }}'
user: '{{ .apps.smtp.user }}'
defaultSecrets:
- key: appKey
- key: dbPassword
- key: smtpPassword
- key: appKey
- key: dbPassword
- key: smtpPassword
requiredSecrets:
- mysql.rootPassword
- mysql.rootPassword

38
bookwyrm/notes.md Normal file
View File

@@ -0,0 +1,38 @@
# BookWyrm — Notes
## SCSS themes must be compiled before collectstatic
BookWyrm ships SCSS source files instead of pre-compiled CSS. The static root is an `emptyDir`
(ephemeral), so it is empty on every pod restart. Running `collectstatic` alone fails:
```
ValueError: Missing staticfiles manifest entry for 'css/themes/...'
```
**Fix**: compile themes before collecting static files:
```yaml
command: ["/bin/sh", "-c"]
args:
- |
python manage.py compile_themes \
&& python manage.py collectstatic --noinput \
&& python manage.py migrate \
&& exec gunicorn bookwyrm.wsgi:application ...
```
Check the image's `Dockerfile` or `docker_start.sh` to confirm the current required build steps.
## PostgreSQL extensions require superuser
BookWyrm uses extensions (`bloom`, `pg_trgm`, etc.) that require superuser to install. Migrations
fail with `permission denied to create extension "bloom"` if the app DB user is not a superuser.
**Fix**: create the extensions in the `db-init-job`, which connects as the `postgres` superuser:
```bash
psql -d "$APP_DB_NAME" -c "CREATE EXTENSION IF NOT EXISTS bloom;"
psql -d "$APP_DB_NAME" -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
```
This must run before migrations (i.e. in `db-init-job`, not in the app startup command).

View File

@@ -80,4 +80,7 @@ spec:
GRANT USAGE ON SCHEMA public TO \"$BOOKWYRM_DB_USER\";
"
# Create bloom extension (required by bookwyrm migrations, needs superuser)
psql -d "$BOOKWYRM_DB_NAME" -c "CREATE EXTENSION IF NOT EXISTS bloom;"
echo "Database initialization completed."

View File

@@ -23,7 +23,7 @@ spec:
containers:
- name: bookwyrm
image: ghcr.io/bookwyrm-social/bookwyrm:v0.8.6
command: ["gunicorn", "bookwyrm.wsgi:application"]
command: ["/bin/sh", "-c", "python manage.py compile_themes && python manage.py collectstatic --noinput && python manage.py migrate && exec gunicorn bookwyrm.wsgi:application"]
ports:
- name: http
containerPort: 8000

View File

@@ -3,10 +3,6 @@ kind: Ingress
metadata:
name: bookwyrm
namespace: bookwyrm
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:
@@ -20,7 +16,3 @@ spec:
name: bookwyrm
port:
number: 80
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}

View File

@@ -1,13 +1,11 @@
version: 0.8.7-1
version: 0.8.7-2
requires:
- name: postgres
- name: redis
- name: smtp
- name: postgres
- name: redis
- name: smtp
defaultConfig:
namespace: bookwyrm
externalDnsDomain: '{{ .cloud.domain }}'
domain: bookwyrm.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
storage: 2Gi
db:
host: '{{ .apps.postgres.host }}'
@@ -22,9 +20,9 @@ defaultConfig:
from: '{{ .apps.smtp.from }}'
user: '{{ .apps.smtp.user }}'
defaultSecrets:
- key: secretKey
- key: dbPassword
- key: smtpPassword
- key: secretKey
- key: dbPassword
- key: smtpPassword
requiredSecrets:
- postgres.password
- redis.password
- postgres.password
- redis.password

View File

@@ -2,4 +2,5 @@ name: cert-manager
is: cert-manager
description: X.509 certificate management for Kubernetes
category: services
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/cert-manager.svg
latest: "v1"

View File

@@ -3,10 +3,6 @@ kind: Ingress
metadata:
name: chamilo
namespace: {{ .namespace }}
annotations:
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
external-dns.alpha.kubernetes.io/target: {{ .externalDnsDomain }}
external-dns.alpha.kubernetes.io/ttl: "60"
spec:
ingressClassName: traefik
rules:
@@ -20,7 +16,3 @@ spec:
name: chamilo
port:
number: 80
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}

View File

@@ -1,19 +1,17 @@
version: 1.11.28-1
version: 1.11.28-2
requires:
- name: mysql
- name: smtp
- name: mysql
- name: smtp
defaultConfig:
namespace: chamilo
externalDnsDomain: '{{ .cloud.domain }}'
domain: chamilo.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
storage: 2Gi
siteName: Chamilo LMS
adminUser: admin
adminEmail: '{{ .operator.email }}'
db:
host: '{{ .apps.mysql.host }}'
port: "3306"
port: '3306'
name: chamilo
user: chamilo
smtp:
@@ -22,8 +20,8 @@ defaultConfig:
from: '{{ .apps.smtp.from }}'
user: '{{ .apps.smtp.user }}'
defaultSecrets:
- key: adminPassword
- key: dbPassword
- key: smtpPassword
- key: adminPassword
- key: dbPassword
- key: smtpPassword
requiredSecrets:
- mysql.rootPassword
- mysql.rootPassword

View File

@@ -3,3 +3,5 @@ is: community-search
description: Community Search is a federated, self-hosted search engine built on community-curated indexes rather than global web crawling.
category: community
latest: "0"
ignoreRules:
- WC-ICON # no established icon available

View File

@@ -2,10 +2,6 @@ apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: community-search
annotations:
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
external-dns.alpha.kubernetes.io/target: {{ .externalDnsDomain }}
external-dns.alpha.kubernetes.io/ttl: "60"
spec:
ingressClassName: traefik
rules:
@@ -19,7 +15,3 @@ spec:
name: community-search
port:
number: 80
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}

View File

@@ -1,12 +1,9 @@
version: 0.1.2_1
requires: []
version: 0.1.2_1-1
defaultConfig:
namespace: community-search
externalDnsDomain: '{{ .cloud.domain }}'
domain: search.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
storage: 2Gi
selfUrl: 'https://search.{{ .cloud.domain }}'
selfName: 'Community Search'
selfUrl: https://search.{{ .cloud.domain }}
selfName: Community Search
defaultSecrets:
- key: adminToken
- key: adminToken

View File

@@ -2,4 +2,5 @@ name: coredns
is: coredns
description: DNS server for internal cluster DNS resolution
category: services
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/coredns.svg
latest: "v1"

View File

@@ -2,4 +2,5 @@ name: crowdsec
is: crowdsec
description: CrowdSec security engine with Traefik bouncer for threat detection and rate limiting
category: services
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/crowdsec.svg
latest: "v1"

View File

@@ -60,7 +60,7 @@ spec:
- name: LEVEL_INFO
value: "true"
- name: AGENT_USERNAME
value: "kubernetes-cluster"
value: "{{ if .agentUsername }}{{ .agentUsername }}{{ else }}kubernetes-cluster{{ end }}"
- name: AGENT_PASSWORD
valueFrom:
secretKeyRef:
@@ -72,6 +72,12 @@ spec:
name: crowdsec-secrets
key: bouncerApiKey
optional: true
{{ if .centralLapiUrl }}
- name: DISABLE_LOCAL_API
value: "true"
- name: LOCAL_API_URL
value: "{{ .centralLapiUrl }}"
{{ end }}
ports:
- name: lapi
containerPort: 8080
@@ -79,6 +85,7 @@ spec:
- name: prometheus
containerPort: 6060
protocol: TCP
{{ if not .centralLapiUrl }}
livenessProbe:
httpGet:
path: /health
@@ -91,6 +98,7 @@ spec:
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
{{ end }}
resources:
requests:
cpu: 50m

View File

@@ -1,4 +1,4 @@
version: v1.7.8-1
version: v1.7.8-4
requires:
- name: longhorn
- name: traefik
@@ -6,6 +6,8 @@ defaultConfig:
namespace: crowdsec
rateLimitAverage: "100"
rateLimitBurst: "100"
centralLapiUrl: ""
agentUsername: ""
defaultSecrets:
- key: agentPassword
- key: bouncerApiKey

View File

@@ -12,7 +12,7 @@ spec:
plugin:
bouncer:
crowdsecLapiScheme: http
crowdsecLapiHost: crowdsec-lapi.crowdsec.svc.cluster.local:8080
crowdsecLapiHost: {{ if .centralLapiUrl }}{{ strings.TrimPrefix "http://" .centralLapiUrl }}{{ else }}crowdsec-lapi.crowdsec.svc.cluster.local:8080{{ end }}
crowdsecLapiKeyFile: /etc/traefik/crowdsec/api-key
crowdsecMode: stream
updateIntervalSeconds: 15

25
cryptpad/notes.md Normal file
View File

@@ -0,0 +1,25 @@
# CryptPad — Notes
## Config directory requires emptyDir + initContainer
CryptPad's `/cryptpad/config/` directory is in the image overlay filesystem and is not writable
by the container process even when running as root.
**Fix**: mount an `emptyDir` at `/cryptpad/config` and use an initContainer to pre-seed
`config.example.js` from the image into the emptyDir:
```yaml
initContainers:
- name: seed-config
image: cryptpad/cryptpad:version-X.Y.Z
command: [sh, -c, "cp /cryptpad/config/config.example.js /config-dest/config.example.js"]
volumeMounts:
- name: cryptpad-config
mountPath: /config-dest
volumes:
- name: cryptpad-config
emptyDir: {}
```
Set `CPAD_CONF=/cryptpad/config/config.js` — the startup script copies `config.example.js` to
`config.js` on first run if `config.js` doesn't exist.

View File

@@ -22,7 +22,7 @@ spec:
type: RuntimeDefault
initContainers:
- name: seed-config
image: cryptpad/cryptpad:latest
image: cryptpad/cryptpad:version-2026.5.1
command:
- sh
- -c
@@ -33,9 +33,18 @@ spec:
volumeMounts:
- name: cryptpad-config
mountPath: /config-dest
securityContext:
runAsNonRoot: false
runAsUser: 0
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
seccompProfile:
type: RuntimeDefault
containers:
- name: cryptpad
image: cryptpad/cryptpad:latest
image: cryptpad/cryptpad:version-2026.5.1
ports:
- name: http
containerPort: 3000
@@ -80,7 +89,14 @@ spec:
periodSeconds: 10
failureThreshold: 3
securityContext:
runAsNonRoot: false
runAsUser: 0
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
seccompProfile:
type: RuntimeDefault
volumes:
- name: cryptpad-data
persistentVolumeClaim:

View File

@@ -3,10 +3,6 @@ kind: Ingress
metadata:
name: cryptpad
namespace: cryptpad
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:
@@ -30,8 +26,3 @@ spec:
name: cryptpad
port:
number: 80
tls:
- hosts:
- {{ .domain }}
- {{ .sandboxDomain }}
secretName: {{ .tlsSecretName }}

View File

@@ -1,10 +1,8 @@
version: 2024.x-1
version: 2026.5.1-3
defaultConfig:
namespace: cryptpad
externalDnsDomain: '{{ .cloud.domain }}'
domain: cryptpad.{{ .cloud.domain }}
sandboxDomain: cryptpad-sandbox.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
storage: 2Gi
defaultSecrets:
- key: adminKey
- key: adminKey

View File

@@ -4,15 +4,8 @@ kind: Ingress
metadata:
name: decidim
namespace: decidim
annotations:
external-dns.alpha.kubernetes.io/target: {{ .externalDnsDomain }}
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
spec:
ingressClassName: traefik
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}
rules:
- host: {{ .domain }}
http:

View File

@@ -1,16 +1,14 @@
version: 0.31.0-2
version: 0.31.0-4
requires:
- name: postgres
- name: redis
- name: smtp
- name: postgres
- name: redis
- name: smtp
defaultConfig:
namespace: decidim
externalDnsDomain: '{{ .cloud.domain }}'
storage: 2Gi
systemAdminEmail: '{{ .operator.email }}'
siteName: 'Decidim'
siteName: Decidim
domain: decidim.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
db:
host: '{{ .apps.postgres.host }}'
port: '{{ .apps.postgres.port }}'
@@ -27,13 +25,14 @@ defaultConfig:
tls: '{{ .apps.smtp.tls }}'
startTls: '{{ .apps.smtp.startTls }}'
defaultSecrets:
- key: systemAdminPassword
- key: secretKeyBase
default: "{{ random.AlphaNum 128 }}"
- key: dbPassword
- key: dbUrl
default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}"
- key: systemAdminPassword
- key: secretKeyBase
default: '{{ random.AlphaNum 128 }}'
- key: dbPassword
- key: dbUrl
default: postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host
}}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable
requiredSecrets:
- postgres.password
- redis.password
- smtp.password
- postgres.password
- redis.password
- smtp.password

View File

@@ -69,6 +69,7 @@ spec:
END IF;
END
\$\$;
ALTER USER $DISCOURSE_DB_USER WITH PASSWORD '$DISCOURSE_DB_PASSWORD';
GRANT ALL PRIVILEGES ON DATABASE $DISCOURSE_DB_NAME TO $DISCOURSE_DB_USER;
GRANT ALL ON SCHEMA public TO $DISCOURSE_DB_USER;
GRANT USAGE ON SCHEMA public TO $DISCOURSE_DB_USER;

View File

@@ -5,10 +5,8 @@ kind: Ingress
metadata:
name: discourse
namespace: "{{ .namespace }}"
annotations:
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
external-dns.alpha.kubernetes.io/target: "{{ .externalDnsDomain }}"
spec:
ingressClassName: traefik
rules:
- host: "{{ .domain }}"
http:
@@ -20,7 +18,3 @@ spec:
name: discourse
port:
name: http
tls:
- hosts:
- "{{ .domain }}"
secretName: wildcard-external-wild-cloud-tls

View File

@@ -1,17 +1,15 @@
version: 3.5.3-3
version: 3.5.3-5
requires:
- name: postgres
- name: redis
- name: smtp
- name: postgres
- name: redis
- name: smtp
defaultConfig:
namespace: discourse
externalDnsDomain: '{{ .cloud.domain }}'
storage: 2Gi
adminEmail: '{{ .operator.email }}'
adminUsername: admin
siteName: 'Community'
siteName: Community
domain: discourse.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
db:
host: '{{ .apps.postgres.host }}'
port: '{{ .apps.postgres.port }}'
@@ -28,13 +26,14 @@ defaultConfig:
tls: '{{ .apps.smtp.tls }}'
startTls: '{{ .apps.smtp.startTls }}'
defaultSecrets:
- key: adminPassword
- key: secretKeyBase
default: "{{ random.Hex 32 }}"
- key: smtpPassword
- key: dbPassword
- key: dbUrl
default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable"
- key: adminPassword
- key: secretKeyBase
default: '{{ random.Hex 32 }}'
- key: smtpPassword
- key: dbPassword
- key: dbUrl
default: postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host
}}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable
requiredSecrets:
- postgres.password
- redis.password
- postgres.password
- redis.password

View File

@@ -2,4 +2,8 @@ name: docker-registry
is: docker-registry
description: Private Docker image registry for cluster
category: services
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/docker.svg
latest: "3"
ignoreRules:
- WC-DNS # internal registry; external DNS not needed

View File

@@ -11,10 +11,7 @@ spec:
matchLabels:
app: docker-registry
strategy:
rollingUpdate:
maxSurge: 0
maxUnavailable: 1
type: RollingUpdate
type: Recreate
template:
metadata:
labels:

View File

@@ -3,6 +3,7 @@ kind: Ingress
metadata:
name: docker-registry
spec:
ingressClassName: traefik
rules:
- host: {{ .host }}
http:
@@ -14,7 +15,3 @@ spec:
name: docker-registry
port:
number: 5000
tls:
- hosts:
- {{ .host }}
secretName: wildcard-internal-wild-cloud-tls

View File

@@ -5,7 +5,8 @@ labels:
- includeSelectors: true
pairs:
app: docker-registry
managedBy: wild-cloud
managedBy: kustomize
partOf: wild-cloud
resources:
- deployment.yaml
- ingress.yaml

View File

@@ -1,8 +1,5 @@
version: "3.0.0"
requires:
- name: traefik
- name: cert-manager
version: 3.0.0-2
defaultConfig:
namespace: docker-registry
host: "registry.{{ .cloud.internalDomain }}"
host: registry.{{ .cloud.internalDomain }}
storage: 5Gi

78
docs/database.md Normal file
View File

@@ -0,0 +1,78 @@
# Database Patterns
## PostgreSQL
### Always use `?sslmode=disable` `[WC-SSL]`
Wild Cloud's internal PostgreSQL has no SSL configured. Without `?sslmode=disable`, connections fail with "The server does not support SSL connections."
```yaml
defaultSecrets:
- key: dbUrl
default: "postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable"
```
Also set `PGSSLMODE=disable` for apps that use libpq directly.
### db-init-job
Include a `db-init-job.yaml` for every app that uses PostgreSQL. See `immich`, `gitea`, or `openproject` for reference implementations. The job must:
- Create the database if it doesn't exist
- Create/update the user with correct credentials
- Grant permissions
- Install required extensions (`vector`, `pg_trgm`, etc.)
- Use `restartPolicy: OnFailure` and `runAsUser: 999`
- Be idempotent — safe to re-run after redeploy
### Database URL secrets
When an app needs a connection URL with embedded credentials, use a `dbUrl` secret — do not construct URLs inline:
```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
```
## MySQL
### db-init user password idempotency `[WC-DBIN]`
`CREATE USER IF NOT EXISTS` only sets the password on first creation. On redeploy against an existing database the password stays stale, causing "Access denied".
Always follow `CREATE USER` with `ALTER USER`:
```sql
CREATE USER IF NOT EXISTS '${DB_USERNAME}'@'%' IDENTIFIED BY '${DB_PASSWORD}';
ALTER USER '${DB_USERNAME}'@'%' IDENTIFIED BY '${DB_PASSWORD}';
GRANT ALL PRIVILEGES ON ${DB_DATABASE_NAME}.* TO '${DB_USERNAME}'@'%';
FLUSH PRIVILEGES;
```
`ALTER USER` is a no-op when the user was just created — it is safe to always include it.
### Required secrets reference
MySQL secrets are copied into `<app>-secrets`, not `mysql-secrets`. Reference them as:
```yaml
secretKeyRef:
name: myapp-secrets
key: mysql.rootPassword # not mysql-secrets / rootPassword
```
## 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`
- **Database URLs**: value must contain `://`
- **Usernames**: include `USER` — these are not patched on restore

44
docs/jvm.md Normal file
View File

@@ -0,0 +1,44 @@
# JVM (Spring Boot / Scala / Kotlin)
## Startup delay
Spring Boot loads the full application context before accepting HTTP. Cold starts typically take 60120 seconds depending on the number of beans and auto-configuration classes.
```yaml
livenessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 120
periodSeconds: 30
failureThreshold: 6
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 90
periodSeconds: 15
failureThreshold: 3
```
If the app exposes a Spring Actuator health endpoint, prefer it for readiness over the root path.
## Heap limit
Set `-Xmx` to keep the heap within the container memory limit. The JVM does not automatically respect cgroup limits on older JDK versions.
```yaml
env:
- name: JAVA_OPTS
value: "-Xmx512m -Xms256m"
resources:
limits:
memory: 768Mi
requests:
memory: 512Mi
```
Keep `-Xmx` 2533% below the container limit to leave room for off-heap memory (metaspace, direct buffers, GC overhead).
## Resources
512Mi1Gi request is typical. JVM startup can spike higher — use `requests` to reserve and `limits` to cap.

44
docs/linuxserver.md Normal file
View File

@@ -0,0 +1,44 @@
# linuxserver.io / s6-overlay images
## Security context
Images using the s6-overlay init system (all linuxserver.io images) must run as root and need full capabilities to switch to their internal `abc` user. **Do not drop capabilities** — s6-overlay's user-switching will fail.
```yaml
spec:
template:
spec:
securityContext: # pod level
runAsNonRoot: false
runAsUser: 0
seccompProfile:
type: RuntimeDefault
containers:
- name: app
securityContext: # container level — only this field
readOnlyRootFilesystem: false
```
Do NOT set `allowPrivilegeEscalation: false` or `capabilities.drop: ALL` for these images.
Suppress `WC-SC-POD` and `WC-SC-CTR` in `app.yaml`:
```yaml
ignoreRules:
- WC-SC-POD # s6-overlay requires root
- WC-SC-CTR # s6-overlay requires full capabilities
```
## PUID / PGID
linuxserver.io images accept `PUID` and `PGID` env vars to set the internal `abc` user's UID/GID. Set them explicitly for consistent file ownership on PVCs:
```yaml
env:
- name: PUID
value: "1000"
- name: PGID
value: "1000"
```
**Affected apps**: BookStack, and any app using a linuxserver.io image.

54
docs/nginx.md Normal file
View File

@@ -0,0 +1,54 @@
# nginx-based images
## Security context
nginx binds to port 80 (or 443) and needs to `chown` files before dropping to `www-data`. It must run as root but can drop most capabilities after startup.
```yaml
spec:
template:
spec:
securityContext: # pod level
runAsNonRoot: false
runAsUser: 0
seccompProfile:
type: RuntimeDefault
containers:
- name: app
securityContext: # container level
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
add: [CHOWN, SETUID, SETGID, NET_BIND_SERVICE]
readOnlyRootFilesystem: false
```
Add `NET_BIND_SERVICE` only if the container binds to a port below 1024. Drop it if the app uses a high port (8080, 3000, etc.).
## Docker DNS resolver (`WC-DKRDNS`)
nginx proxy configs shipped for Docker often contain:
```nginx
resolver 127.0.0.11 valid=3s;
set $backend app-backend:8000;
proxy_pass http://$backend$request_uri;
```
`127.0.0.11` is Docker's embedded DNS resolver — it does not exist in Kubernetes. This fails with `send() failed (111: Connection refused) while resolving`.
There's a second trap: **any nginx variable anywhere in the `proxy_pass` URL** (even `$request_uri` in the path) forces runtime DNS resolution and requires a `resolver` directive. Removing the resolver line but keeping `proxy_pass http://backend:8000$request_uri;` still fails with `no resolver defined to resolve backend`.
**Fix**: override the nginx config template via a ConfigMap mounted with `subPath`, and remove the variable from `proxy_pass` entirely:
```nginx
# Instead of:
resolver 127.0.0.11 valid=3s;
set $backend ${BACKEND};
proxy_pass http://$backend$request_uri;
# Use:
proxy_pass http://${BACKEND};
```
With no nginx variable in `proxy_pass`, nginx resolves the hostname at startup using the pod's `/etc/resolv.conf` (which points to CoreDNS). The full request URI is still forwarded automatically in a regex `location ~` block. Mount the ConfigMap with `subPath` to override just the template file without replacing the whole directory.

20
docs/nodejs.md Normal file
View File

@@ -0,0 +1,20 @@
# Node.js
## Memory limits
Node.js apps frequently OOM-crash at the default 512Mi limit. Set a higher limit and cap the V8 heap so it stays under it:
```yaml
resources:
limits:
memory: 1Gi
requests:
memory: 512Mi
env:
- name: NODE_OPTIONS
value: "--max-old-space-size=768"
```
`--max-old-space-size` is in MiB. Keep it 2533% below the container limit so the process has headroom for non-heap memory (buffers, native modules, etc.).
**Applies to**: NocoDB, Outline, Gitea (web), and any Electron/Express/Next.js app.

31
docs/php.md Normal file
View File

@@ -0,0 +1,31 @@
# PHP (Laravel / Symfony)
## Startup delay
Laravel's startup sequence (autoload optimization, key generation, migrations, package discovery) commonly takes 23 minutes on cluster restart or first boot. The default `initialDelaySeconds: 60` is too short — the liveness probe fires before the app is ready, kills the container, and a restart loop begins.
**Symptom**: pod shows `Running` for ~2 minutes, then gets `Killing` due to liveness probe failure, then a new pod starts the same cycle.
Use a `tcpSocket` liveness probe (avoids the Host header issue) with an extended initial delay:
```yaml
livenessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 120
periodSeconds: 30
failureThreshold: 6
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 60
periodSeconds: 15
failureThreshold: 3
```
Symfony has the same behavior. Adjust `initialDelaySeconds` up if migrations are slow.
## Resources
256512Mi request is typical for a PHP-FPM or Artisan-served app. Workers (queue:work, horizon) are separate processes — give them their own Deployment and 256512Mi each.

65
docs/python.md Normal file
View File

@@ -0,0 +1,65 @@
# Python (Django / FastAPI)
## Health probe host header
Kubernetes sends probe requests directly to the pod IP. Django's `ALLOWED_HOSTS` rejects requests without a matching `Host` header, causing every probe to return 400 and the pod to restart. Add a `Host` header to all HTTP probes:
```yaml
livenessProbe:
httpGet:
path: /
port: 8000
httpHeaders:
- name: Host
value: "{{ .domain }}"
readinessProbe:
httpGet:
path: /
port: 8000
httpHeaders:
- name: Host
value: "{{ .domain }}"
```
FastAPI and Flask apps with explicit host validation have the same issue.
## Startup delay (apps that run migrations)
Django apps that run `migrate` on startup need extra time before the liveness probe fires. The default 60-second delay is too short for large migration sets.
```yaml
livenessProbe:
httpGet:
path: /
port: 8000
httpHeaders:
- name: Host
value: "{{ .domain }}"
initialDelaySeconds: 90
periodSeconds: 30
failureThreshold: 6
readinessProbe:
httpGet:
path: /
port: 8000
httpHeaders:
- name: Host
value: "{{ .domain }}"
initialDelaySeconds: 60
periodSeconds: 15
failureThreshold: 3
```
## Celery workers
Celery workers are full Python processes. Size them at 256512Mi and deploy them as a separate Deployment from the web process. They do not need an HTTP probe — use a `tcpSocket` or `exec` check if a liveness probe is needed at all.
## Non-interactive superuser creation
```bash
kubectl exec -n <ns> <pod> -- \
env DJANGO_SUPERUSER_PASSWORD="${PASSWORD}" \
python manage.py createsuperuser --email "${EMAIL}" --noinput
```
`--noinput` reads the password from `DJANGO_SUPERUSER_PASSWORD`.

33
docs/redis.md Normal file
View File

@@ -0,0 +1,33 @@
# Redis
## Authenticated Redis URL
Wild Cloud's Redis requires a password. Apps that take a Redis URL must embed the password. Use Kubernetes env var expansion so the password isn't hardcoded:
```yaml
requiredSecrets:
- redis.password
# In deployment env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-secrets
key: redis.password
- name: REDIS_URL
value: "redis://:$(REDIS_PASSWORD)@{{ .redis.host }}:6379"
```
`$(REDIS_PASSWORD)` is evaluated by Kubernetes at pod start from other env vars in the same container spec.
## Apps that don't support Redis passwords
Some apps only accept `REDIS_HOST` and `REDIS_PORT` with no password option (e.g. Ushahidi). Deploy a dedicated unauthenticated Redis sidecar for those apps and remove `redis` from `requires` in `manifest.yaml`:
```yaml
- name: redis
image: redis:7-alpine
args: ["--save", ""] # disable persistence
```
Point the app's `REDIS_HOST` at the sidecar's service name.

32
docs/ruby.md Normal file
View File

@@ -0,0 +1,32 @@
# Ruby (Rails)
## Startup delay
Rails startup (bundle exec, asset precompilation, initializer chain) typically takes 3090 seconds. Extend the liveness probe delay to avoid restart loops:
```yaml
livenessProbe:
tcpSocket:
port: 3000
initialDelaySeconds: 90
periodSeconds: 30
failureThreshold: 6
readinessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 60
periodSeconds: 15
failureThreshold: 3
```
## Sidekiq workers
Sidekiq is a full Ruby process, not a thread pool. Deploy it as a separate Deployment from the web process with 256512Mi memory. It does not need an HTTP liveness probe — omit it or use a simple `exec` check.
## Non-interactive rails console / rake tasks
```bash
kubectl exec -n <ns> <pod> -- bundle exec rake db:migrate
kubectl exec -n <ns> <pod> -- bundle exec rails runner "User.create!(...)"
```

45
docs/scripts.md Normal file
View File

@@ -0,0 +1,45 @@
# 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.
## Registering scripts
Register every script in `manifest.yaml` — the web UI shows a button with 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
```
## Script conventions
Follow `synapse/versions/v1/scripts/create-user.sh` as the reference implementation:
- Require `KUBECONFIG`, `WILD_INSTANCE`, and `WILD_API_DATA_DIR`; exit with a clear error if missing
- Read `namespace` from `config.yaml` via `yq` — never hardcode it
- Auto-generate passwords with `openssl rand` if `PASSWORD` is not supplied
- Find the running pod by label — never hardcode a pod name
- Print credentials at the end with a "save this — it won't be shown again" warning
## Common commands
**Django non-interactive superuser**:
```bash
kubectl exec -n <ns> <pod> -- \
env DJANGO_SUPERUSER_PASSWORD="${PASSWORD}" \
python manage.py createsuperuser --email "${EMAIL}" --noinput
```
**Rails**:
```bash
kubectl exec -n <ns> <pod> -- bundle exec rake db:migrate
```
**Affected apps**: Eventyay, Synapse, Headscale.

47
docs/traefik.md Normal file
View File

@@ -0,0 +1,47 @@
# Traefik
## "No available server" after redeploy `[WC-SEL]`
If a Deployment was originally created without kustomize labels in its selector, re-deploying cannot fix it — `spec.selector` is immutable once set. The Service selector updates (it's mutable) but pods won't match it, leaving the Service with no endpoints.
**Symptom**: Traefik returns "No available server" even though pods are Running.
**Diagnosis**:
```bash
kubectl get endpoints <app> -n <namespace>
# shows <none> — pods aren't matching the service
kubectl get pods -n <namespace> --show-labels
# pod labels don't include app: <name> / managedBy: kustomize / partOf: wild-cloud
```
**Fix**: delete the Deployment and re-deploy:
```bash
kubectl delete deployment <name> -n <ns>
wild app deploy <app>
```
## Same-origin iframes
The global Traefik `security-headers` middleware sets `X-Frame-Options: SAMEORIGIN`. Apps that embed their own sub-pages in iframes (e.g. Etherpad's pad editor) work correctly with this setting.
`DENY` was the previous setting and broke same-origin iframes. If you see:
```
Blocked a frame with origin "https://..." from accessing a cross-origin frame
```
the app is likely creating a same-origin iframe that a stale `DENY` header is blocking. Verify the cluster's Traefik middleware is using `SAMEORIGIN`.
**Note**: route-level Traefik middlewares run before entrypoint middlewares on the response path. A per-app middleware cannot override the global one.
## TLS-terminating reverse proxy (DISABLE_HTTPS)
Some apps (e.g. Zulip) redirect port 80 → HTTPS internally. When Traefik terminates TLS and forwards plain HTTP, this causes an infinite redirect loop.
Set these env vars to disable the internal redirect and trust the forwarded proto:
```yaml
- name: DISABLE_HTTPS
value: "true"
- name: LOADBALANCER_IPS
value: "10.244.0.0/16" # Kubernetes pod CIDR (Flannel default)
```
Also update liveness/readiness probes from HTTPS port 443 to HTTP port 80.

View File

@@ -3,6 +3,8 @@ is: e2e-test-app
description: End-to-end test application for automated integration testing. Includes PVC and PostgreSQL dependency to exercise all backup strategies.
category: services
latest: "2"
ignoreRules:
- WC-ICON # test/QA app; not user-facing
upgrade:
from:
- version: ">=1.0.0"

View File

@@ -1,11 +1,9 @@
version: 1.0.0-1
version: 1.0.0-2
requires:
- name: postgres
- name: postgres
defaultConfig:
namespace: e2e-test-app
domain: e2e-test-app.{{ .cloud.domain }}
externalDnsDomain: '{{ .cloud.domain }}'
tlsSecretName: wildcard-wild-cloud-tls
storage: 1Gi
db:
host: '{{ .apps.postgres.host }}'
@@ -13,8 +11,9 @@ defaultConfig:
name: e2e_test_app
user: e2e_test_app
defaultSecrets:
- key: dbPassword
- key: dbUrl
default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable"
- key: dbPassword
- key: dbUrl
default: postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host
}}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable
requiredSecrets:
- postgres.password
- postgres.password

View File

@@ -1,12 +1,10 @@
version: 2.0.0-1
version: 2.0.0-2
requires:
- name: postgres
- name: mysql
- name: postgres
- name: mysql
defaultConfig:
namespace: e2e-test-app
domain: e2e-test-app.{{ .cloud.domain }}
externalDnsDomain: '{{ .cloud.domain }}'
tlsSecretName: wildcard-wild-cloud-tls
storage: 1Gi
db:
host: '{{ .apps.postgres.host }}'
@@ -19,10 +17,11 @@ defaultConfig:
name: e2e_test_app
user: e2e_test_app
defaultSecrets:
- key: dbPassword
- key: dbUrl
default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable"
- key: mysqlPassword
- key: dbPassword
- key: dbUrl
default: postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host
}}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable
- key: mysqlPassword
requiredSecrets:
- postgres.password
- mysql.rootPassword
- postgres.password
- mysql.rootPassword

View File

@@ -3,10 +3,6 @@ kind: Ingress
metadata:
name: etherpad
namespace: etherpad
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:
@@ -20,7 +16,3 @@ spec:
name: etherpad
port:
number: 80
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}

View File

@@ -1,11 +1,9 @@
version: 2.2.7-1
version: 2.2.7-2
requires:
- name: postgres
- name: postgres
defaultConfig:
namespace: etherpad
externalDnsDomain: '{{ .cloud.domain }}'
domain: etherpad.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
storage: 2Gi
title: Etherpad
db:
@@ -14,9 +12,10 @@ defaultConfig:
name: etherpad
user: etherpad
defaultSecrets:
- key: adminPassword
- key: dbPassword
- key: dbUrl
default: 'postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable'
- key: adminPassword
- key: dbPassword
- key: dbUrl
default: postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host
}}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable
requiredSecrets:
- postgres.password
- postgres.password

View File

@@ -4,3 +4,5 @@ description: Eventyay is an open-source event management platform covering ticke
category: community
icon: https://raw.githubusercontent.com/fossasia/eventyay/main/app/eventyay/static/common/img/logo.svg
latest: "1"
ignoreRules:
- WC-IMG # no versioned Docker tags upstream; main tag is intentional

View File

@@ -3,10 +3,6 @@ kind: Ingress
metadata:
name: eventyay
namespace: {{ .namespace }}
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:
@@ -20,7 +16,3 @@ spec:
name: eventyay
port:
number: 80
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}

View File

@@ -1,13 +1,11 @@
version: main-5
version: main-6
requires:
- name: postgres
- name: redis
- name: smtp
- name: postgres
- name: redis
- name: smtp
defaultConfig:
namespace: eventyay
externalDnsDomain: '{{ .cloud.domain }}'
domain: eventyay.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
storage: 2Gi
timezone: UTC
db:
@@ -24,19 +22,19 @@ defaultConfig:
from: '{{ .apps.smtp.from }}'
user: '{{ .apps.smtp.user }}'
scripts:
- name: create-superuser
path: scripts/create-superuser.sh
description: "Create an Eventyay superuser account for first-time setup."
params:
- name: EMAIL
description: Email address for the superuser account
required: true
- name: PASSWORD
description: Password (leave blank to generate a random one)
- name: create-superuser
path: scripts/create-superuser.sh
description: Create an Eventyay superuser account for first-time setup.
params:
- name: EMAIL
description: Email address for the superuser account
required: true
- name: PASSWORD
description: Password (leave blank to generate a random one)
defaultSecrets:
- key: dbPassword
- key: djangoSecret
- key: dbPassword
- key: djangoSecret
requiredSecrets:
- postgres.password
- redis.password
- smtp.password
- postgres.password
- redis.password
- smtp.password

View File

@@ -3,3 +3,6 @@ is: example
description: An example application that is deployed with internal-only access.
category: services
latest: "1"
ignoreRules:
- WC-ICON # example/demo app
- WC-IMG # example/demo app intentionally uses latest for simplicity

View File

@@ -15,6 +15,9 @@ spec:
labels:
app: example-admin
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: example-admin
image: nginx:latest
@@ -28,6 +31,15 @@ spec:
requests:
cpu: 50m
memory: 10Mi
securityContext:
runAsNonRoot: false
runAsUser: 0
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
seccompProfile:
type: RuntimeDefault
livenessProbe:
httpGet:
path: /

View File

@@ -4,6 +4,7 @@ kind: Ingress
metadata:
name: example-admin
spec:
ingressClassName: traefik
rules:
- host: "{{ .host }}"
http:
@@ -15,7 +16,3 @@ spec:
name: example-admin
port:
number: 80
tls:
- hosts:
- "{{ .host }}"
secretName: wildcard-internal-wild-cloud-tls

View File

@@ -1,6 +1,4 @@
version: 1.0.0-1
version: 1.0.0-3
defaultConfig:
namespace: example-admin
externalDnsDomain: '{{ .cloud.domain }}'
host: '{{ .host }}'
tlsSecretName: wildcard-internal-wild-cloud-tls

View File

@@ -3,3 +3,5 @@ is: example
description: An example application that is deployed with public access.
category: services
latest: "1"
ignoreRules:
- WC-ICON # example/demo app

View File

@@ -13,6 +13,9 @@ spec:
labels:
app: example-app
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: example-app
image: nginx:alpine
@@ -26,6 +29,15 @@ spec:
requests:
cpu: 50m
memory: 32Mi
securityContext:
runAsNonRoot: false
runAsUser: 0
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
seccompProfile:
type: RuntimeDefault
livenessProbe:
httpGet:
path: /

View File

@@ -4,8 +4,6 @@ kind: Ingress
metadata:
name: example-app
annotations:
external-dns.alpha.kubernetes.io/target: "{{ .cloud.externalDnsTarget }}"
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
# Optional: Enable basic auth
# traefik.ingress.kubernetes.io/auth-type: basic
# traefik.ingress.kubernetes.io/auth-secret: basic-auth
@@ -22,7 +20,3 @@ spec:
name: example-app
port:
number: 80
tls:
- hosts:
- "{{ .host }}"
secretName: wildcard-wild-cloud-tls

View File

@@ -1,6 +1,4 @@
version: 1.0.0-1
version: 1.0.0-2
defaultConfig:
namespace: example-app
externalDnsDomain: '{{ .cloud.domain }}'
host: example-app.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls

View File

@@ -3,3 +3,7 @@ is: externaldns
description: Automatically configures DNS records for services
category: services
latest: "v0"
ignoreRules:
- WC-ICON # infrastructure service; not user-facing
- WC-SEL # infrastructure service; selectors are managed by the upstream manifest
- WC-NS # fixed namespace: externaldns

View File

@@ -16,6 +16,10 @@ spec:
app: external-dns
spec:
serviceAccountName: external-dns
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: external-dns
image: registry.k8s.io/external-dns/external-dns:v0.13.4
@@ -24,12 +28,21 @@ spec:
- --source=ingress
- --txt-owner-id={{ .ownerId }}
- --provider=cloudflare
- --domain-filter=payne.io
#- --exclude-domains=internal.${DOMAIN}
{{- with (index . "domainFilter") }}
- --domain-filter={{ . }}
{{- end }}
{{- with (index . "internalDomain") }}
- --exclude-domains={{ . }}
{{- end }}
- --cloudflare-dns-records-per-page=5000
- --publish-internal-services
- --no-cloudflare-proxied
- --log-level=debug
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: true
env:
- name: CF_API_TOKEN
valueFrom:

View File

@@ -1,10 +1,11 @@
version: v0.13.4-1
version: v0.13.4-6
deploymentName: external-dns
requires:
- name: cert-manager
defaultConfig:
namespace: externaldns
ownerId: "wild-cloud-{{ .cluster.name }}"
internalDomain: "{{ .cloud.internalDomain }}"
defaultSecrets:
- key: cloudflareToken
requiredSecrets:

View File

@@ -3,10 +3,6 @@ kind: Ingress
metadata:
name: firefly-iii
namespace: firefly-iii
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:
@@ -20,7 +16,3 @@ spec:
name: firefly-iii
port:
number: 80
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}

View File

@@ -1,11 +1,9 @@
version: 6.6.3-1
version: 6.6.3-2
requires:
- name: postgres
- name: postgres
defaultConfig:
namespace: firefly-iii
externalDnsDomain: '{{ .cloud.domain }}'
domain: firefly-iii.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
storage: 1Gi
timezone: UTC
adminEmail: '{{ .operator.email }}'
@@ -15,7 +13,7 @@ defaultConfig:
name: fireflyiii
user: fireflyiii
defaultSecrets:
- key: appKey
- key: dbPassword
- key: appKey
- key: dbPassword
requiredSecrets:
- postgres.password
- postgres.password

View File

@@ -3,10 +3,6 @@ kind: Ingress
metadata:
name: formbricks
namespace: {{ .namespace }}
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:
@@ -20,7 +16,3 @@ spec:
name: formbricks
port:
number: 80
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}

View File

@@ -1,27 +1,26 @@
version: 3.6.0-1
version: 3.6.0-2
requires:
- name: postgres
- name: redis
- name: postgres
- name: redis
defaultConfig:
namespace: formbricks
externalDnsDomain: "{{ .cloud.domain }}"
domain: formbricks.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
storage: 2Gi
db:
host: "{{ .apps.postgres.host }}"
port: "{{ .apps.postgres.port }}"
host: '{{ .apps.postgres.host }}'
port: '{{ .apps.postgres.port }}'
name: formbricks
user: formbricks
redis:
host: "{{ .apps.redis.host }}"
host: '{{ .apps.redis.host }}'
defaultSecrets:
- key: dbPassword
- key: dbUrl
default: "postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable"
- key: nextauthSecret
- key: encryptionKey
- key: cronSecret
- key: dbPassword
- key: dbUrl
default: postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host
}}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable
- key: nextauthSecret
- key: encryptionKey
- key: cronSecret
requiredSecrets:
- postgres.password
- redis.password
- postgres.password
- redis.password

21
gancio/notes.md Normal file
View File

@@ -0,0 +1,21 @@
# Gancio — Notes
## "Non empty db" crash on re-deploy
Gancio stores its setup state in `config.json` on the data PVC. If the PVC is lost or the app is
deleted and redeployed against an existing database, Gancio finds a non-empty DB but no
`config.json` and refuses to start: `"Non empty db! Please move your current db elsewhere than retry."`
**Fix**: drop and recreate the public schema, then restart the deployment:
```bash
kubectl exec -n postgres <postgres-pod> -- psql -U postgres gancio \
-c "DROP SCHEMA public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO gancio; GRANT ALL ON SCHEMA public TO public;"
kubectl scale deployment -n gancio gancio --replicas=0
# wait for pod to terminate
kubectl scale deployment -n gancio gancio --replicas=1
```
Scale down before dropping the schema to release the database connection first.
This only affects re-deployments; fresh installs work fine.

View File

@@ -28,6 +28,6 @@ Key settings in `config.yaml`:
## Notes
- **Re-deploy warning**: If the app is deleted and redeployed with an existing database, Gancio will refuse to start with a "Non empty db" error. Fix by dropping and recreating the public schema in the `gancio` database before restarting. See ADDING-APPS-NOTES.md note 18 for the exact commands.
- **Re-deploy warning**: If the app is deleted and redeployed with an existing database, Gancio will refuse to start with a "Non empty db" error. See `gancio/notes.md` for the fix.
- Gancio stores its setup state in `config.json` on the data PVC — do not delete the PVC without also clearing the database
- Events are federated to Mastodon and other ActivityPub platforms

View File

@@ -3,10 +3,6 @@ kind: Ingress
metadata:
name: gancio
namespace: gancio
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:
@@ -20,7 +16,3 @@ spec:
name: gancio
port:
number: 80
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}

View File

@@ -1,11 +1,9 @@
version: 1.28.2-1
version: 1.28.2-2
requires:
- name: postgres
- name: postgres
defaultConfig:
namespace: gancio
externalDnsDomain: '{{ .cloud.domain }}'
domain: gancio.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
storage: 2Gi
db:
host: '{{ .apps.postgres.host }}'
@@ -13,8 +11,9 @@ defaultConfig:
name: gancio
user: gancio
defaultSecrets:
- key: dbPassword
- key: dbUrl
default: 'postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable'
- key: dbPassword
- key: dbUrl
default: postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host
}}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable
requiredSecrets:
- postgres.password
- postgres.password

15
ghost/notes.md Normal file
View File

@@ -0,0 +1,15 @@
# Ghost — Notes
## Bitnami image migration
Ghost was originally packaged using `bitnami/ghost`. Bitnami moved their images off Docker Hub
in 2023, so the Bitnami image no longer pulls without authentication.
**Fix**: use the official `ghost:X.Y.Z-alpine` image. The official image has different conventions:
| | Bitnami | Official |
|---|---|---|
| DB host env | `GHOST_DATABASE_HOST` | `database__connection__host` |
| Content path | `/bitnami/ghost` | `/var/lib/ghost/content` |
Check `docker manifest inspect ghost:latest` for the current version tag before updating.

View File

@@ -19,6 +19,7 @@ spec:
mysql -h ${DB_HOSTNAME} -P ${DB_PORT} -u root -p${MYSQL_ROOT_PASSWORD} <<EOF
CREATE DATABASE IF NOT EXISTS ${DB_DATABASE_NAME};
CREATE USER IF NOT EXISTS '${DB_USERNAME}'@'%' IDENTIFIED BY '${DB_PASSWORD}';
ALTER USER '${DB_USERNAME}'@'%' IDENTIFIED BY '${DB_PASSWORD}';
GRANT ALL PRIVILEGES ON ${DB_DATABASE_NAME}.* TO '${DB_USERNAME}'@'%';
FLUSH PRIVILEGES;
EOF

View File

@@ -3,10 +3,6 @@ kind: Ingress
metadata:
name: ghost
namespace: ghost
annotations:
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
external-dns.alpha.kubernetes.io/target: {{ .externalDnsDomain }}
external-dns.alpha.kubernetes.io/ttl: "60"
spec:
ingressClassName: traefik
rules:
@@ -20,7 +16,3 @@ spec:
name: ghost
port:
number: 80
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}

View File

@@ -1,12 +1,10 @@
version: 5.130.6-1
version: 5.130.6-2
requires:
- name: mysql
- name: smtp
- name: mysql
- name: smtp
defaultConfig:
namespace: ghost
externalDnsDomain: '{{ .cloud.domain }}'
domain: ghost.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
storage: 2Gi
db:
host: '{{ .apps.mysql.host }}'
@@ -19,7 +17,7 @@ defaultConfig:
from: '{{ .apps.smtp.from }}'
user: '{{ .apps.smtp.user }}'
defaultSecrets:
- key: dbPassword
- key: smtpPassword
- key: dbPassword
- key: smtpPassword
requiredSecrets:
- mysql.rootPassword
- mysql.rootPassword

View File

@@ -19,7 +19,11 @@ spec:
partOf: wild-cloud
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
terminationGracePeriodSeconds: 60
containers:
- name: gitea
@@ -78,7 +82,10 @@ spec:
resources:
{}
securityContext:
{}
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
volumeMounts:
- name: temp
mountPath: /tmp

View File

@@ -3,10 +3,8 @@ kind: Ingress
metadata:
name: gitea-public
namespace: gitea
annotations:
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
external-dns.alpha.kubernetes.io/target: "{{ .externalDnsDomain }}"
spec:
ingressClassName: traefik
rules:
- host: "{{ .domain }}"
http:
@@ -18,7 +16,3 @@ spec:
name: gitea-http
port:
number: 3000
tls:
- secretName: "{{ .tlsSecretName }}"
hosts:
- "{{ .domain }}"

View File

@@ -1,16 +1,14 @@
version: 1.24.3-2
version: 1.24.3-5
requires:
- name: postgres
- name: smtp
- name: postgres
- name: smtp
defaultConfig:
namespace: gitea
externalDnsDomain: '{{ .cloud.domain }}'
appName: Gitea
domain: gitea.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
storage: 2Gi
adminUser: admin
adminEmail: "{{ .operator.email }}"
adminEmail: '{{ .operator.email }}'
db:
name: gitea
user: gitea
@@ -22,10 +20,10 @@ defaultConfig:
user: '{{ .apps.smtp.user }}'
from: '{{ .apps.smtp.from }}'
defaultSecrets:
- key: adminPassword
- key: dbPassword
- key: secretKey
- key: jwtSecret
- key: smtpPassword
- key: adminPassword
- key: dbPassword
- key: secretKey
- key: jwtSecret
- key: smtpPassword
requiredSecrets:
- postgres.password
- postgres.password

View File

@@ -2,4 +2,5 @@ name: headlamp
is: headlamp
description: Modern Kubernetes web UI (SIG UI) with in-cluster authentication
category: services
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/headlamp.svg
latest: "v0"

View File

@@ -1,64 +1,18 @@
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: internal-only
name: headlamp
namespace: headlamp
spec:
ipWhiteList:
sourceRange:
- 127.0.0.1/32
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: headlamp-redirect-scheme
namespace: headlamp
spec:
redirectScheme:
scheme: https
permanent: true
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: headlamp-https
namespace: headlamp
spec:
entryPoints:
- websecure
routes:
- match: Host(`headlamp.{{ .internalDomain }}`)
kind: Rule
middlewares:
- name: internal-only
namespace: headlamp
services:
- name: headlamp
port: 80
tls:
secretName: wildcard-internal-wild-cloud-tls
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: headlamp-http
namespace: headlamp
spec:
entryPoints:
- web
routes:
- match: Host(`headlamp.{{ .internalDomain }}`)
kind: Rule
middlewares:
- name: headlamp-redirect-scheme
namespace: headlamp
services:
- name: headlamp
port: 80
ingressClassName: traefik
rules:
- host: headlamp.{{ .internalDomain }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: headlamp
port:
number: 80

View File

@@ -1,11 +1,13 @@
version: v0.42.0
requires:
- name: traefik
- name: cert-manager
version: v0.42.0-1
defaultConfig:
namespace: headlamp
internalDomain: "{{ .cloud.internalDomain }}"
internalDomain: '{{ .cloud.internalDomain }}'
central:
ipAllow:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
deploy:
waitForRollout:
name: headlamp
timeout: "120s"
timeout: 120s

36
headscale/notes.md Normal file
View File

@@ -0,0 +1,36 @@
# Headscale — Notes
## v0.29+ CLI: inconsistent user identifier types
Different commands in headscale v0.29 accept different user identifiers:
| Command | Accepts |
|---|---|
| `preauthkeys create --user <id>` | numeric ID only |
| `auth register --user <username>` | username string |
| `users destroy --identifier <id>` | numeric ID only (not positional) |
Always list users first to get the ID:
```bash
headscale users list
headscale preauthkeys create --user 1 # numeric ID
headscale auth register --auth-id <id> --user payne # username OK
headscale users destroy --identifier 1 # numeric ID
```
## Node registration via browser
When a Tailscale client connects via browser (not pre-auth key), headscale shows a command like:
```
headscale auth register --auth-id hskey-authreq-XXXX --user USERNAME
```
Run it via:
```bash
kubectl exec -n headscale deploy/headscale -- \
headscale auth register --auth-id <id> --user <username>
```

View File

@@ -3,10 +3,6 @@ kind: Ingress
metadata:
name: headscale
namespace: headscale
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:
@@ -20,7 +16,3 @@ spec:
name: headscale
port:
number: 80
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}

View File

@@ -1,19 +1,18 @@
version: 0.29.1-1
version: 0.29.1-2
scripts:
- name: register-node
description: Register a Tailscale node using the auth ID shown in the Tailscale app.
path: scripts/register-node.sh
params:
- name: AUTH_ID
description: The auth ID shown by the Tailscale app (e.g. hskey-authreq-...)
required: true
- name: USER
description: Headscale username to register the node under
required: true
- name: register-node
description: Register a Tailscale node using the auth ID shown in the Tailscale
app.
path: scripts/register-node.sh
params:
- name: AUTH_ID
description: The auth ID shown by the Tailscale app (e.g. hskey-authreq-...)
required: true
- name: USER
description: Headscale username to register the node under
required: true
defaultConfig:
namespace: headscale
domain: headscale.{{ .cloud.domain }}
externalDnsDomain: "{{ .cloud.domain }}"
tlsSecretName: wildcard-wild-cloud-tls
storage: 2Gi
vpnBaseDomain: vpn.{{ .cloud.domain }}

View File

@@ -16,6 +16,11 @@ spec:
app: immich-machine-learning
component: machine-learning
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- image: "ghcr.io/immich-app/immich-machine-learning:v1.135.3"
name: immich-machine-learning
@@ -25,6 +30,11 @@ spec:
env:
- name: TZ
value: "UTC"
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
volumeMounts:
- mountPath: /cache
name: immich-cache

View File

@@ -9,16 +9,18 @@ spec:
matchLabels:
app: immich-microservices
strategy:
rollingUpdate:
maxSurge: 0
maxUnavailable: 1
type: RollingUpdate
type: Recreate
template:
metadata:
labels:
app: immich-microservices
component: microservices
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- image: "ghcr.io/immich-app/immich-server:v1.135.3"
name: immich-microservices
@@ -43,6 +45,11 @@ spec:
value: "UTC"
- name: IMMICH_WORKERS_EXCLUDE
value: api
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
volumeMounts:
- mountPath: /usr/src/app/upload
name: immich-storage

View File

@@ -9,16 +9,18 @@ spec:
matchLabels:
app: immich-server
strategy:
rollingUpdate:
maxSurge: 0
maxUnavailable: 1
type: RollingUpdate
type: Recreate
template:
metadata:
labels:
app: immich-server
component: server
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- image: "ghcr.io/immich-app/immich-server:v1.135.3"
name: immich-server
@@ -46,6 +48,11 @@ spec:
value: "UTC"
- name: IMMICH_WORKERS_EXCLUDE
value: microservices
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
volumeMounts:
- mountPath: /usr/src/app/upload
name: immich-storage

View File

@@ -3,10 +3,8 @@ apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: immich-public
annotations:
external-dns.alpha.kubernetes.io/target: "{{ .externalDnsDomain }}"
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
spec:
ingressClassName: traefik
rules:
- host: "{{ .domain }}"
http:
@@ -18,7 +16,3 @@ spec:
name: immich-server
port:
number: 3001
tls:
- secretName: wildcard-wild-cloud-tls
hosts:
- "{{ .domain }}"

View File

@@ -1,14 +1,12 @@
version: 1.135.3-2
version: 1.135.3-5
requires:
- name: redis
- name: postgres
- name: redis
- name: postgres
defaultConfig:
namespace: immich
externalDnsDomain: '{{ .cloud.domain }}'
storage: 5Gi
cacheStorage: 5Gi
domain: immich.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
db:
host: '{{ .apps.postgres.host }}'
name: immich
@@ -16,7 +14,7 @@ defaultConfig:
redis:
host: '{{ .apps.redis.host }}'
defaultSecrets:
- key: dbPassword
- key: dbPassword
requiredSecrets:
- redis.password
- postgres.password
- redis.password
- postgres.password

View File

@@ -3,10 +3,6 @@ kind: Ingress
metadata:
name: indico
namespace: {{ .namespace }}
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:
@@ -20,7 +16,3 @@ spec:
name: indico
port:
number: 80
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}

View File

@@ -1,32 +1,30 @@
version: 3.3.12-1
version: 3.3.12-2
requires:
- name: postgres
- name: redis
- name: smtp
- name: postgres
- name: redis
- name: smtp
defaultConfig:
namespace: indico
externalDnsDomain: "{{ .cloud.domain }}"
domain: "indico.{{ .cloud.domain }}"
tlsSecretName: wildcard-wild-cloud-tls
domain: indico.{{ .cloud.domain }}
storage: 2Gi
timezone: UTC
db:
host: "{{ .apps.postgres.host }}"
port: "{{ .apps.postgres.port }}"
host: '{{ .apps.postgres.host }}'
port: '{{ .apps.postgres.port }}'
name: indico
user: indico
redis:
host: "{{ .apps.redis.host }}"
port: "6379"
host: '{{ .apps.redis.host }}'
port: '6379'
smtp:
host: "{{ .apps.smtp.host }}"
port: "{{ .apps.smtp.port }}"
from: "{{ .apps.smtp.from }}"
user: "{{ .apps.smtp.user }}"
host: '{{ .apps.smtp.host }}'
port: '{{ .apps.smtp.port }}'
from: '{{ .apps.smtp.from }}'
user: '{{ .apps.smtp.user }}'
defaultSecrets:
- key: dbPassword
- key: secretKey
- key: dbPassword
- key: secretKey
requiredSecrets:
- postgres.password
- redis.password
- smtp.password
- postgres.password
- redis.password
- smtp.password

Some files were not shown because too many files have changed in this diff Show More