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
This commit is contained in:
2026-07-02 21:34:27 +00:00
parent 9f5057dff8
commit 4d983819c9
151 changed files with 3403 additions and 1303 deletions

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.