Compare commits
8 Commits
9f5057dff8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
763ac922d5 | ||
|
|
1b3200a5f1 | ||
|
|
dc5518b57f | ||
|
|
bb5a76b864 | ||
|
|
219a28bc65 | ||
|
|
387838819b | ||
|
|
c80d8dc411 | ||
|
|
4d983819c9 |
@@ -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 2–3 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 10–15 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 | |
|
|
||||||
1167
ADDING-APPS.md
1167
ADDING-APPS.md
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
- @README.md
|
- @README.md
|
||||||
|
|
||||||
- ADDING-APPS.md is the expert advice you need when adding new apps to the Wild Cloud Directory.
|
- 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
|
## Finding good sources of documentation for adding a new app to the Wild Cloud Directory
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: akaunting
|
name: akaunting
|
||||||
namespace: 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:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
@@ -20,7 +16,3 @@ spec:
|
|||||||
name: akaunting
|
name: akaunting
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .domain }}
|
|
||||||
secretName: {{ .tlsSecretName }}
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
version: 3.1.21-1
|
version: 3.1.21-2
|
||||||
requires:
|
requires:
|
||||||
- name: mysql
|
- name: mysql
|
||||||
- name: smtp
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: akaunting
|
namespace: akaunting
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
domain: akaunting.{{ .cloud.domain }}
|
domain: akaunting.{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
locale: en-US
|
locale: en-US
|
||||||
dbPrefix: aka_
|
dbPrefix: aka_
|
||||||
@@ -15,7 +13,7 @@ defaultConfig:
|
|||||||
adminEmail: '{{ .operator.email }}'
|
adminEmail: '{{ .operator.email }}'
|
||||||
db:
|
db:
|
||||||
host: '{{ .apps.mysql.host }}'
|
host: '{{ .apps.mysql.host }}'
|
||||||
port: "3306"
|
port: '3306'
|
||||||
name: akaunting
|
name: akaunting
|
||||||
user: akaunting
|
user: akaunting
|
||||||
smtp:
|
smtp:
|
||||||
@@ -24,8 +22,8 @@ defaultConfig:
|
|||||||
from: '{{ .apps.smtp.from }}'
|
from: '{{ .apps.smtp.from }}'
|
||||||
user: '{{ .apps.smtp.user }}'
|
user: '{{ .apps.smtp.user }}'
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: adminPassword
|
- key: adminPassword
|
||||||
- key: smtpPassword
|
- key: smtpPassword
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
- mysql.rootPassword
|
- mysql.rootPassword
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ apiVersion: networking.k8s.io/v1
|
|||||||
kind: Ingress
|
kind: Ingress
|
||||||
metadata:
|
metadata:
|
||||||
name: aptly
|
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:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
@@ -19,7 +15,3 @@ spec:
|
|||||||
name: aptly
|
name: aptly
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .domain }}
|
|
||||||
secretName: {{ .tlsSecretName }}
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
version: 1.6.2-3
|
version: 1.6.2-4
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: aptly
|
namespace: aptly
|
||||||
externalDnsDomain: "{{ .cloud.domain }}"
|
domain: aptly.{{ .cloud.domain }}
|
||||||
domain: "aptly.{{ .cloud.domain }}"
|
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 5Gi
|
storage: 5Gi
|
||||||
apiUser: aptly
|
apiUser: aptly
|
||||||
operatorEmail: "{{ .operator.email }}"
|
operatorEmail: '{{ .operator.email }}'
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: apiPassword
|
- key: apiPassword
|
||||||
|
|||||||
@@ -69,6 +69,15 @@ spec:
|
|||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: baserow-data
|
- name: baserow-data
|
||||||
mountPath: /baserow/data
|
mountPath: /baserow/data
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: false
|
||||||
|
runAsUser: 0
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /api/_health/
|
path: /api/_health/
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: baserow
|
name: baserow
|
||||||
namespace: {{ .namespace }}
|
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:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
@@ -20,7 +16,3 @@ spec:
|
|||||||
name: baserow
|
name: baserow
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .domain }}
|
|
||||||
secretName: {{ .tlsSecretName }}
|
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
version: 2.2.2-1
|
version: 2.2.2-3
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
- name: redis
|
- name: redis
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: baserow
|
namespace: baserow
|
||||||
externalDnsDomain: "{{ .cloud.domain }}"
|
|
||||||
domain: baserow.{{ .cloud.domain }}
|
domain: baserow.{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
db:
|
db:
|
||||||
host: "{{ .apps.postgres.host }}"
|
host: '{{ .apps.postgres.host }}'
|
||||||
port: "5432"
|
port: '5432'
|
||||||
name: baserow
|
name: baserow
|
||||||
user: baserow
|
user: baserow
|
||||||
redis:
|
redis:
|
||||||
host: "{{ .apps.redis.host }}"
|
host: '{{ .apps.redis.host }}'
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: secretKey
|
- key: secretKey
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
- postgres.password
|
- postgres.password
|
||||||
- redis.password
|
- redis.password
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: bookstack
|
name: bookstack
|
||||||
namespace: {{ .namespace }}
|
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:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
@@ -20,7 +16,3 @@ spec:
|
|||||||
name: bookstack
|
name: bookstack
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .domain }}
|
|
||||||
secretName: {{ .tlsSecretName }}
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
version: 26.05.1-2
|
version: 26.05.1-3
|
||||||
requires:
|
requires:
|
||||||
- name: mysql
|
- name: mysql
|
||||||
- name: smtp
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: bookstack
|
namespace: bookstack
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
domain: bookstack.{{ .cloud.domain }}
|
domain: bookstack.{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
db:
|
db:
|
||||||
host: '{{ .apps.mysql.host }}'
|
host: '{{ .apps.mysql.host }}'
|
||||||
@@ -19,8 +17,8 @@ defaultConfig:
|
|||||||
from: '{{ .apps.smtp.from }}'
|
from: '{{ .apps.smtp.from }}'
|
||||||
user: '{{ .apps.smtp.user }}'
|
user: '{{ .apps.smtp.user }}'
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: appKey
|
- key: appKey
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: smtpPassword
|
- key: smtpPassword
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
- mysql.rootPassword
|
- mysql.rootPassword
|
||||||
|
|||||||
38
bookwyrm/notes.md
Normal file
38
bookwyrm/notes.md
Normal 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).
|
||||||
@@ -80,4 +80,7 @@ spec:
|
|||||||
GRANT USAGE ON SCHEMA public TO \"$BOOKWYRM_DB_USER\";
|
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."
|
echo "Database initialization completed."
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ spec:
|
|||||||
containers:
|
containers:
|
||||||
- name: bookwyrm
|
- name: bookwyrm
|
||||||
image: ghcr.io/bookwyrm-social/bookwyrm:v0.8.6
|
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:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
containerPort: 8000
|
containerPort: 8000
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: bookwyrm
|
name: bookwyrm
|
||||||
namespace: 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:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
@@ -20,7 +16,3 @@ spec:
|
|||||||
name: bookwyrm
|
name: bookwyrm
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .domain }}
|
|
||||||
secretName: {{ .tlsSecretName }}
|
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
version: 0.8.7-1
|
version: 0.8.7-2
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
- name: redis
|
- name: redis
|
||||||
- name: smtp
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: bookwyrm
|
namespace: bookwyrm
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
domain: bookwyrm.{{ .cloud.domain }}
|
domain: bookwyrm.{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
db:
|
db:
|
||||||
host: '{{ .apps.postgres.host }}'
|
host: '{{ .apps.postgres.host }}'
|
||||||
@@ -22,9 +20,9 @@ defaultConfig:
|
|||||||
from: '{{ .apps.smtp.from }}'
|
from: '{{ .apps.smtp.from }}'
|
||||||
user: '{{ .apps.smtp.user }}'
|
user: '{{ .apps.smtp.user }}'
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: secretKey
|
- key: secretKey
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: smtpPassword
|
- key: smtpPassword
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
- postgres.password
|
- postgres.password
|
||||||
- redis.password
|
- redis.password
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ name: cert-manager
|
|||||||
is: cert-manager
|
is: cert-manager
|
||||||
description: X.509 certificate management for Kubernetes
|
description: X.509 certificate management for Kubernetes
|
||||||
category: services
|
category: services
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/cert-manager.svg
|
||||||
latest: "v1"
|
latest: "v1"
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: chamilo
|
name: chamilo
|
||||||
namespace: {{ .namespace }}
|
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:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
@@ -20,7 +16,3 @@ spec:
|
|||||||
name: chamilo
|
name: chamilo
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .domain }}
|
|
||||||
secretName: {{ .tlsSecretName }}
|
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
version: 1.11.28-1
|
version: 1.11.28-2
|
||||||
requires:
|
requires:
|
||||||
- name: mysql
|
- name: mysql
|
||||||
- name: smtp
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: chamilo
|
namespace: chamilo
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
domain: chamilo.{{ .cloud.domain }}
|
domain: chamilo.{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
siteName: Chamilo LMS
|
siteName: Chamilo LMS
|
||||||
adminUser: admin
|
adminUser: admin
|
||||||
adminEmail: '{{ .operator.email }}'
|
adminEmail: '{{ .operator.email }}'
|
||||||
db:
|
db:
|
||||||
host: '{{ .apps.mysql.host }}'
|
host: '{{ .apps.mysql.host }}'
|
||||||
port: "3306"
|
port: '3306'
|
||||||
name: chamilo
|
name: chamilo
|
||||||
user: chamilo
|
user: chamilo
|
||||||
smtp:
|
smtp:
|
||||||
@@ -22,8 +20,8 @@ defaultConfig:
|
|||||||
from: '{{ .apps.smtp.from }}'
|
from: '{{ .apps.smtp.from }}'
|
||||||
user: '{{ .apps.smtp.user }}'
|
user: '{{ .apps.smtp.user }}'
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: adminPassword
|
- key: adminPassword
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: smtpPassword
|
- key: smtpPassword
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
- mysql.rootPassword
|
- mysql.rootPassword
|
||||||
|
|||||||
@@ -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.
|
description: Community Search is a federated, self-hosted search engine built on community-curated indexes rather than global web crawling.
|
||||||
category: community
|
category: community
|
||||||
latest: "0"
|
latest: "0"
|
||||||
|
ignoreRules:
|
||||||
|
- WC-ICON # no established icon available
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ apiVersion: networking.k8s.io/v1
|
|||||||
kind: Ingress
|
kind: Ingress
|
||||||
metadata:
|
metadata:
|
||||||
name: community-search
|
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:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
@@ -19,7 +15,3 @@ spec:
|
|||||||
name: community-search
|
name: community-search
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .domain }}
|
|
||||||
secretName: {{ .tlsSecretName }}
|
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
version: 0.1.2_1
|
version: 0.1.2_1-1
|
||||||
requires: []
|
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: community-search
|
namespace: community-search
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
domain: search.{{ .cloud.domain }}
|
domain: search.{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
selfUrl: 'https://search.{{ .cloud.domain }}'
|
selfUrl: https://search.{{ .cloud.domain }}
|
||||||
selfName: 'Community Search'
|
selfName: Community Search
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: adminToken
|
- key: adminToken
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ name: coredns
|
|||||||
is: coredns
|
is: coredns
|
||||||
description: DNS server for internal cluster DNS resolution
|
description: DNS server for internal cluster DNS resolution
|
||||||
category: services
|
category: services
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/coredns.svg
|
||||||
latest: "v1"
|
latest: "v1"
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ name: crowdsec
|
|||||||
is: crowdsec
|
is: crowdsec
|
||||||
description: CrowdSec security engine with Traefik bouncer for threat detection and rate limiting
|
description: CrowdSec security engine with Traefik bouncer for threat detection and rate limiting
|
||||||
category: services
|
category: services
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/crowdsec.svg
|
||||||
latest: "v1"
|
latest: "v1"
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ spec:
|
|||||||
- name: LEVEL_INFO
|
- name: LEVEL_INFO
|
||||||
value: "true"
|
value: "true"
|
||||||
- name: AGENT_USERNAME
|
- name: AGENT_USERNAME
|
||||||
value: "kubernetes-cluster"
|
value: "{{ if .agentUsername }}{{ .agentUsername }}{{ else }}kubernetes-cluster{{ end }}"
|
||||||
- name: AGENT_PASSWORD
|
- name: AGENT_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
@@ -72,6 +72,12 @@ spec:
|
|||||||
name: crowdsec-secrets
|
name: crowdsec-secrets
|
||||||
key: bouncerApiKey
|
key: bouncerApiKey
|
||||||
optional: true
|
optional: true
|
||||||
|
{{ if .centralLapiUrl }}
|
||||||
|
- name: DISABLE_LOCAL_API
|
||||||
|
value: "true"
|
||||||
|
- name: LOCAL_API_URL
|
||||||
|
value: "{{ .centralLapiUrl }}"
|
||||||
|
{{ end }}
|
||||||
ports:
|
ports:
|
||||||
- name: lapi
|
- name: lapi
|
||||||
containerPort: 8080
|
containerPort: 8080
|
||||||
@@ -79,6 +85,7 @@ spec:
|
|||||||
- name: prometheus
|
- name: prometheus
|
||||||
containerPort: 6060
|
containerPort: 6060
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
|
{{ if not .centralLapiUrl }}
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health
|
||||||
@@ -91,6 +98,7 @@ spec:
|
|||||||
port: 8080
|
port: 8080
|
||||||
initialDelaySeconds: 10
|
initialDelaySeconds: 10
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
|
{{ end }}
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: 50m
|
cpu: 50m
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
version: v1.7.8-1
|
version: v1.7.8-4
|
||||||
requires:
|
requires:
|
||||||
- name: longhorn
|
- name: longhorn
|
||||||
- name: traefik
|
- name: traefik
|
||||||
@@ -6,6 +6,8 @@ defaultConfig:
|
|||||||
namespace: crowdsec
|
namespace: crowdsec
|
||||||
rateLimitAverage: "100"
|
rateLimitAverage: "100"
|
||||||
rateLimitBurst: "100"
|
rateLimitBurst: "100"
|
||||||
|
centralLapiUrl: ""
|
||||||
|
agentUsername: ""
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: agentPassword
|
- key: agentPassword
|
||||||
- key: bouncerApiKey
|
- key: bouncerApiKey
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ spec:
|
|||||||
plugin:
|
plugin:
|
||||||
bouncer:
|
bouncer:
|
||||||
crowdsecLapiScheme: http
|
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
|
crowdsecLapiKeyFile: /etc/traefik/crowdsec/api-key
|
||||||
crowdsecMode: stream
|
crowdsecMode: stream
|
||||||
updateIntervalSeconds: 15
|
updateIntervalSeconds: 15
|
||||||
|
|||||||
25
cryptpad/notes.md
Normal file
25
cryptpad/notes.md
Normal 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.
|
||||||
@@ -22,7 +22,7 @@ spec:
|
|||||||
type: RuntimeDefault
|
type: RuntimeDefault
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: seed-config
|
- name: seed-config
|
||||||
image: cryptpad/cryptpad:latest
|
image: cryptpad/cryptpad:version-2026.5.1
|
||||||
command:
|
command:
|
||||||
- sh
|
- sh
|
||||||
- -c
|
- -c
|
||||||
@@ -33,9 +33,18 @@ spec:
|
|||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: cryptpad-config
|
- name: cryptpad-config
|
||||||
mountPath: /config-dest
|
mountPath: /config-dest
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: false
|
||||||
|
runAsUser: 0
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
containers:
|
containers:
|
||||||
- name: cryptpad
|
- name: cryptpad
|
||||||
image: cryptpad/cryptpad:latest
|
image: cryptpad/cryptpad:version-2026.5.1
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
containerPort: 3000
|
containerPort: 3000
|
||||||
@@ -80,7 +89,14 @@ spec:
|
|||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
securityContext:
|
securityContext:
|
||||||
|
runAsNonRoot: false
|
||||||
|
runAsUser: 0
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
readOnlyRootFilesystem: false
|
readOnlyRootFilesystem: false
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
volumes:
|
volumes:
|
||||||
- name: cryptpad-data
|
- name: cryptpad-data
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: cryptpad
|
name: cryptpad
|
||||||
namespace: 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:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
@@ -30,8 +26,3 @@ spec:
|
|||||||
name: cryptpad
|
name: cryptpad
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .domain }}
|
|
||||||
- {{ .sandboxDomain }}
|
|
||||||
secretName: {{ .tlsSecretName }}
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
version: 2024.x-1
|
version: 2026.5.1-3
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: cryptpad
|
namespace: cryptpad
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
domain: cryptpad.{{ .cloud.domain }}
|
domain: cryptpad.{{ .cloud.domain }}
|
||||||
sandboxDomain: cryptpad-sandbox.{{ .cloud.domain }}
|
sandboxDomain: cryptpad-sandbox.{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: adminKey
|
- key: adminKey
|
||||||
|
|||||||
@@ -4,15 +4,8 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: decidim
|
name: decidim
|
||||||
namespace: decidim
|
namespace: decidim
|
||||||
annotations:
|
|
||||||
external-dns.alpha.kubernetes.io/target: {{ .externalDnsDomain }}
|
|
||||||
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
|
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .domain }}
|
|
||||||
secretName: {{ .tlsSecretName }}
|
|
||||||
rules:
|
rules:
|
||||||
- host: {{ .domain }}
|
- host: {{ .domain }}
|
||||||
http:
|
http:
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
version: 0.31.0-2
|
version: 0.31.0-4
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
- name: redis
|
- name: redis
|
||||||
- name: smtp
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: decidim
|
namespace: decidim
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
systemAdminEmail: '{{ .operator.email }}'
|
systemAdminEmail: '{{ .operator.email }}'
|
||||||
siteName: 'Decidim'
|
siteName: Decidim
|
||||||
domain: decidim.{{ .cloud.domain }}
|
domain: decidim.{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
db:
|
db:
|
||||||
host: '{{ .apps.postgres.host }}'
|
host: '{{ .apps.postgres.host }}'
|
||||||
port: '{{ .apps.postgres.port }}'
|
port: '{{ .apps.postgres.port }}'
|
||||||
@@ -27,13 +25,14 @@ defaultConfig:
|
|||||||
tls: '{{ .apps.smtp.tls }}'
|
tls: '{{ .apps.smtp.tls }}'
|
||||||
startTls: '{{ .apps.smtp.startTls }}'
|
startTls: '{{ .apps.smtp.startTls }}'
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: systemAdminPassword
|
- key: systemAdminPassword
|
||||||
- key: secretKeyBase
|
- key: secretKeyBase
|
||||||
default: "{{ random.AlphaNum 128 }}"
|
default: '{{ random.AlphaNum 128 }}'
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: dbUrl
|
- key: dbUrl
|
||||||
default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}"
|
default: postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host
|
||||||
|
}}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
- postgres.password
|
- postgres.password
|
||||||
- redis.password
|
- redis.password
|
||||||
- smtp.password
|
- smtp.password
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ spec:
|
|||||||
END IF;
|
END IF;
|
||||||
END
|
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 PRIVILEGES ON DATABASE $DISCOURSE_DB_NAME TO $DISCOURSE_DB_USER;
|
||||||
GRANT ALL ON SCHEMA public TO $DISCOURSE_DB_USER;
|
GRANT ALL ON SCHEMA public TO $DISCOURSE_DB_USER;
|
||||||
GRANT USAGE ON SCHEMA public TO $DISCOURSE_DB_USER;
|
GRANT USAGE ON SCHEMA public TO $DISCOURSE_DB_USER;
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: discourse
|
name: discourse
|
||||||
namespace: "{{ .namespace }}"
|
namespace: "{{ .namespace }}"
|
||||||
annotations:
|
|
||||||
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
|
|
||||||
external-dns.alpha.kubernetes.io/target: "{{ .externalDnsDomain }}"
|
|
||||||
spec:
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
- host: "{{ .domain }}"
|
- host: "{{ .domain }}"
|
||||||
http:
|
http:
|
||||||
@@ -20,7 +18,3 @@ spec:
|
|||||||
name: discourse
|
name: discourse
|
||||||
port:
|
port:
|
||||||
name: http
|
name: http
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- "{{ .domain }}"
|
|
||||||
secretName: wildcard-external-wild-cloud-tls
|
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
version: 3.5.3-3
|
version: 3.5.3-5
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
- name: redis
|
- name: redis
|
||||||
- name: smtp
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: discourse
|
namespace: discourse
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
adminEmail: '{{ .operator.email }}'
|
adminEmail: '{{ .operator.email }}'
|
||||||
adminUsername: admin
|
adminUsername: admin
|
||||||
siteName: 'Community'
|
siteName: Community
|
||||||
domain: discourse.{{ .cloud.domain }}
|
domain: discourse.{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
db:
|
db:
|
||||||
host: '{{ .apps.postgres.host }}'
|
host: '{{ .apps.postgres.host }}'
|
||||||
port: '{{ .apps.postgres.port }}'
|
port: '{{ .apps.postgres.port }}'
|
||||||
@@ -28,13 +26,14 @@ defaultConfig:
|
|||||||
tls: '{{ .apps.smtp.tls }}'
|
tls: '{{ .apps.smtp.tls }}'
|
||||||
startTls: '{{ .apps.smtp.startTls }}'
|
startTls: '{{ .apps.smtp.startTls }}'
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: adminPassword
|
- key: adminPassword
|
||||||
- key: secretKeyBase
|
- key: secretKeyBase
|
||||||
default: "{{ random.Hex 32 }}"
|
default: '{{ random.Hex 32 }}'
|
||||||
- key: smtpPassword
|
- key: smtpPassword
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: dbUrl
|
- key: dbUrl
|
||||||
default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable"
|
default: postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host
|
||||||
|
}}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
- postgres.password
|
- postgres.password
|
||||||
- redis.password
|
- redis.password
|
||||||
|
|||||||
@@ -2,4 +2,8 @@ name: docker-registry
|
|||||||
is: docker-registry
|
is: docker-registry
|
||||||
description: Private Docker image registry for cluster
|
description: Private Docker image registry for cluster
|
||||||
category: services
|
category: services
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/docker.svg
|
||||||
latest: "3"
|
latest: "3"
|
||||||
|
|
||||||
|
ignoreRules:
|
||||||
|
- WC-DNS # internal registry; external DNS not needed
|
||||||
|
|||||||
@@ -11,10 +11,7 @@ spec:
|
|||||||
matchLabels:
|
matchLabels:
|
||||||
app: docker-registry
|
app: docker-registry
|
||||||
strategy:
|
strategy:
|
||||||
rollingUpdate:
|
type: Recreate
|
||||||
maxSurge: 0
|
|
||||||
maxUnavailable: 1
|
|
||||||
type: RollingUpdate
|
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: docker-registry
|
name: docker-registry
|
||||||
spec:
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
- host: {{ .host }}
|
- host: {{ .host }}
|
||||||
http:
|
http:
|
||||||
@@ -14,7 +15,3 @@ spec:
|
|||||||
name: docker-registry
|
name: docker-registry
|
||||||
port:
|
port:
|
||||||
number: 5000
|
number: 5000
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .host }}
|
|
||||||
secretName: wildcard-internal-wild-cloud-tls
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ labels:
|
|||||||
- includeSelectors: true
|
- includeSelectors: true
|
||||||
pairs:
|
pairs:
|
||||||
app: docker-registry
|
app: docker-registry
|
||||||
managedBy: wild-cloud
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
resources:
|
resources:
|
||||||
- deployment.yaml
|
- deployment.yaml
|
||||||
- ingress.yaml
|
- ingress.yaml
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
version: "3.0.0"
|
version: 3.0.0-2
|
||||||
requires:
|
|
||||||
- name: traefik
|
|
||||||
- name: cert-manager
|
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: docker-registry
|
namespace: docker-registry
|
||||||
host: "registry.{{ .cloud.internalDomain }}"
|
host: registry.{{ .cloud.internalDomain }}
|
||||||
storage: 5Gi
|
storage: 5Gi
|
||||||
|
|||||||
78
docs/database.md
Normal file
78
docs/database.md
Normal 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
44
docs/jvm.md
Normal 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 60–120 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` 25–33% below the container limit to leave room for off-heap memory (metaspace, direct buffers, GC overhead).
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
512Mi–1Gi request is typical. JVM startup can spike higher — use `requests` to reserve and `limits` to cap.
|
||||||
44
docs/linuxserver.md
Normal file
44
docs/linuxserver.md
Normal 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
54
docs/nginx.md
Normal 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
20
docs/nodejs.md
Normal 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 25–33% 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
31
docs/php.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# PHP (Laravel / Symfony)
|
||||||
|
|
||||||
|
## Startup delay
|
||||||
|
|
||||||
|
Laravel's startup sequence (autoload optimization, key generation, migrations, package discovery) commonly takes 2–3 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
|
||||||
|
|
||||||
|
256–512Mi request is typical for a PHP-FPM or Artisan-served app. Workers (queue:work, horizon) are separate processes — give them their own Deployment and 256–512Mi each.
|
||||||
65
docs/python.md
Normal file
65
docs/python.md
Normal 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 256–512Mi 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
33
docs/redis.md
Normal 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
32
docs/ruby.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Ruby (Rails)
|
||||||
|
|
||||||
|
## Startup delay
|
||||||
|
|
||||||
|
Rails startup (bundle exec, asset precompilation, initializer chain) typically takes 30–90 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 256–512Mi 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
45
docs/scripts.md
Normal 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
47
docs/traefik.md
Normal 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.
|
||||||
@@ -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.
|
description: End-to-end test application for automated integration testing. Includes PVC and PostgreSQL dependency to exercise all backup strategies.
|
||||||
category: services
|
category: services
|
||||||
latest: "2"
|
latest: "2"
|
||||||
|
ignoreRules:
|
||||||
|
- WC-ICON # test/QA app; not user-facing
|
||||||
upgrade:
|
upgrade:
|
||||||
from:
|
from:
|
||||||
- version: ">=1.0.0"
|
- version: ">=1.0.0"
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
version: 1.0.0-1
|
version: 1.0.0-2
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: e2e-test-app
|
namespace: e2e-test-app
|
||||||
domain: e2e-test-app.{{ .cloud.domain }}
|
domain: e2e-test-app.{{ .cloud.domain }}
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 1Gi
|
storage: 1Gi
|
||||||
db:
|
db:
|
||||||
host: '{{ .apps.postgres.host }}'
|
host: '{{ .apps.postgres.host }}'
|
||||||
@@ -13,8 +11,9 @@ defaultConfig:
|
|||||||
name: e2e_test_app
|
name: e2e_test_app
|
||||||
user: e2e_test_app
|
user: e2e_test_app
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: dbUrl
|
- key: dbUrl
|
||||||
default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable"
|
default: postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host
|
||||||
|
}}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
- postgres.password
|
- postgres.password
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
version: 2.0.0-1
|
version: 2.0.0-2
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
- name: mysql
|
- name: mysql
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: e2e-test-app
|
namespace: e2e-test-app
|
||||||
domain: e2e-test-app.{{ .cloud.domain }}
|
domain: e2e-test-app.{{ .cloud.domain }}
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 1Gi
|
storage: 1Gi
|
||||||
db:
|
db:
|
||||||
host: '{{ .apps.postgres.host }}'
|
host: '{{ .apps.postgres.host }}'
|
||||||
@@ -19,10 +17,11 @@ defaultConfig:
|
|||||||
name: e2e_test_app
|
name: e2e_test_app
|
||||||
user: e2e_test_app
|
user: e2e_test_app
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: dbUrl
|
- key: dbUrl
|
||||||
default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable"
|
default: postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host
|
||||||
- key: mysqlPassword
|
}}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable
|
||||||
|
- key: mysqlPassword
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
- postgres.password
|
- postgres.password
|
||||||
- mysql.rootPassword
|
- mysql.rootPassword
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: etherpad
|
name: etherpad
|
||||||
namespace: 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:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
@@ -20,7 +16,3 @@ spec:
|
|||||||
name: etherpad
|
name: etherpad
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .domain }}
|
|
||||||
secretName: {{ .tlsSecretName }}
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
version: 2.2.7-1
|
version: 2.2.7-2
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: etherpad
|
namespace: etherpad
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
domain: etherpad.{{ .cloud.domain }}
|
domain: etherpad.{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
title: Etherpad
|
title: Etherpad
|
||||||
db:
|
db:
|
||||||
@@ -14,9 +12,10 @@ defaultConfig:
|
|||||||
name: etherpad
|
name: etherpad
|
||||||
user: etherpad
|
user: etherpad
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: adminPassword
|
- key: adminPassword
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: dbUrl
|
- key: dbUrl
|
||||||
default: 'postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable'
|
default: postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host
|
||||||
|
}}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
- postgres.password
|
- postgres.password
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ description: Eventyay is an open-source event management platform covering ticke
|
|||||||
category: community
|
category: community
|
||||||
icon: https://raw.githubusercontent.com/fossasia/eventyay/main/app/eventyay/static/common/img/logo.svg
|
icon: https://raw.githubusercontent.com/fossasia/eventyay/main/app/eventyay/static/common/img/logo.svg
|
||||||
latest: "1"
|
latest: "1"
|
||||||
|
ignoreRules:
|
||||||
|
- WC-IMG # no versioned Docker tags upstream; main tag is intentional
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: eventyay
|
name: eventyay
|
||||||
namespace: {{ .namespace }}
|
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:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
@@ -20,7 +16,3 @@ spec:
|
|||||||
name: eventyay
|
name: eventyay
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .domain }}
|
|
||||||
secretName: {{ .tlsSecretName }}
|
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
version: main-5
|
version: main-6
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
- name: redis
|
- name: redis
|
||||||
- name: smtp
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: eventyay
|
namespace: eventyay
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
domain: eventyay.{{ .cloud.domain }}
|
domain: eventyay.{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
timezone: UTC
|
timezone: UTC
|
||||||
db:
|
db:
|
||||||
@@ -24,19 +22,19 @@ defaultConfig:
|
|||||||
from: '{{ .apps.smtp.from }}'
|
from: '{{ .apps.smtp.from }}'
|
||||||
user: '{{ .apps.smtp.user }}'
|
user: '{{ .apps.smtp.user }}'
|
||||||
scripts:
|
scripts:
|
||||||
- name: create-superuser
|
- name: create-superuser
|
||||||
path: scripts/create-superuser.sh
|
path: scripts/create-superuser.sh
|
||||||
description: "Create an Eventyay superuser account for first-time setup."
|
description: Create an Eventyay superuser account for first-time setup.
|
||||||
params:
|
params:
|
||||||
- name: EMAIL
|
- name: EMAIL
|
||||||
description: Email address for the superuser account
|
description: Email address for the superuser account
|
||||||
required: true
|
required: true
|
||||||
- name: PASSWORD
|
- name: PASSWORD
|
||||||
description: Password (leave blank to generate a random one)
|
description: Password (leave blank to generate a random one)
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: djangoSecret
|
- key: djangoSecret
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
- postgres.password
|
- postgres.password
|
||||||
- redis.password
|
- redis.password
|
||||||
- smtp.password
|
- smtp.password
|
||||||
|
|||||||
@@ -3,3 +3,6 @@ is: example
|
|||||||
description: An example application that is deployed with internal-only access.
|
description: An example application that is deployed with internal-only access.
|
||||||
category: services
|
category: services
|
||||||
latest: "1"
|
latest: "1"
|
||||||
|
ignoreRules:
|
||||||
|
- WC-ICON # example/demo app
|
||||||
|
- WC-IMG # example/demo app intentionally uses latest for simplicity
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: example-admin
|
app: example-admin
|
||||||
spec:
|
spec:
|
||||||
|
securityContext:
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
containers:
|
containers:
|
||||||
- name: example-admin
|
- name: example-admin
|
||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
@@ -28,6 +31,15 @@ spec:
|
|||||||
requests:
|
requests:
|
||||||
cpu: 50m
|
cpu: 50m
|
||||||
memory: 10Mi
|
memory: 10Mi
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: false
|
||||||
|
runAsUser: 0
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: /
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: example-admin
|
name: example-admin
|
||||||
spec:
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
- host: "{{ .host }}"
|
- host: "{{ .host }}"
|
||||||
http:
|
http:
|
||||||
@@ -15,7 +16,3 @@ spec:
|
|||||||
name: example-admin
|
name: example-admin
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- "{{ .host }}"
|
|
||||||
secretName: wildcard-internal-wild-cloud-tls
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
version: 1.0.0-1
|
version: 1.0.0-3
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: example-admin
|
namespace: example-admin
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
host: '{{ .host }}'
|
host: '{{ .host }}'
|
||||||
tlsSecretName: wildcard-internal-wild-cloud-tls
|
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ is: example
|
|||||||
description: An example application that is deployed with public access.
|
description: An example application that is deployed with public access.
|
||||||
category: services
|
category: services
|
||||||
latest: "1"
|
latest: "1"
|
||||||
|
ignoreRules:
|
||||||
|
- WC-ICON # example/demo app
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: example-app
|
app: example-app
|
||||||
spec:
|
spec:
|
||||||
|
securityContext:
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
containers:
|
containers:
|
||||||
- name: example-app
|
- name: example-app
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
@@ -26,6 +29,15 @@ spec:
|
|||||||
requests:
|
requests:
|
||||||
cpu: 50m
|
cpu: 50m
|
||||||
memory: 32Mi
|
memory: 32Mi
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: false
|
||||||
|
runAsUser: 0
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: /
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: example-app
|
name: example-app
|
||||||
annotations:
|
annotations:
|
||||||
external-dns.alpha.kubernetes.io/target: "{{ .cloud.externalDnsTarget }}"
|
|
||||||
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
|
|
||||||
# Optional: Enable basic auth
|
# Optional: Enable basic auth
|
||||||
# traefik.ingress.kubernetes.io/auth-type: basic
|
# traefik.ingress.kubernetes.io/auth-type: basic
|
||||||
# traefik.ingress.kubernetes.io/auth-secret: basic-auth
|
# traefik.ingress.kubernetes.io/auth-secret: basic-auth
|
||||||
@@ -22,7 +20,3 @@ spec:
|
|||||||
name: example-app
|
name: example-app
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- "{{ .host }}"
|
|
||||||
secretName: wildcard-wild-cloud-tls
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
version: 1.0.0-1
|
version: 1.0.0-2
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: example-app
|
namespace: example-app
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
host: example-app.{{ .cloud.domain }}
|
host: example-app.{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
|
|||||||
@@ -3,3 +3,7 @@ is: externaldns
|
|||||||
description: Automatically configures DNS records for services
|
description: Automatically configures DNS records for services
|
||||||
category: services
|
category: services
|
||||||
latest: "v0"
|
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
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ spec:
|
|||||||
app: external-dns
|
app: external-dns
|
||||||
spec:
|
spec:
|
||||||
serviceAccountName: external-dns
|
serviceAccountName: external-dns
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
containers:
|
containers:
|
||||||
- name: external-dns
|
- name: external-dns
|
||||||
image: registry.k8s.io/external-dns/external-dns:v0.13.4
|
image: registry.k8s.io/external-dns/external-dns:v0.13.4
|
||||||
@@ -24,12 +28,21 @@ spec:
|
|||||||
- --source=ingress
|
- --source=ingress
|
||||||
- --txt-owner-id={{ .ownerId }}
|
- --txt-owner-id={{ .ownerId }}
|
||||||
- --provider=cloudflare
|
- --provider=cloudflare
|
||||||
- --domain-filter=payne.io
|
{{- with (index . "domainFilter") }}
|
||||||
#- --exclude-domains=internal.${DOMAIN}
|
- --domain-filter={{ . }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with (index . "internalDomain") }}
|
||||||
|
- --exclude-domains={{ . }}
|
||||||
|
{{- end }}
|
||||||
- --cloudflare-dns-records-per-page=5000
|
- --cloudflare-dns-records-per-page=5000
|
||||||
- --publish-internal-services
|
- --publish-internal-services
|
||||||
- --no-cloudflare-proxied
|
- --no-cloudflare-proxied
|
||||||
- --log-level=debug
|
- --log-level=debug
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
env:
|
env:
|
||||||
- name: CF_API_TOKEN
|
- name: CF_API_TOKEN
|
||||||
valueFrom:
|
valueFrom:
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
version: v0.13.4-1
|
version: v0.13.4-6
|
||||||
deploymentName: external-dns
|
deploymentName: external-dns
|
||||||
requires:
|
requires:
|
||||||
- name: cert-manager
|
- name: cert-manager
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: externaldns
|
namespace: externaldns
|
||||||
ownerId: "wild-cloud-{{ .cluster.name }}"
|
ownerId: "wild-cloud-{{ .cluster.name }}"
|
||||||
|
internalDomain: "{{ .cloud.internalDomain }}"
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: cloudflareToken
|
- key: cloudflareToken
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: firefly-iii
|
name: firefly-iii
|
||||||
namespace: 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:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
@@ -20,7 +16,3 @@ spec:
|
|||||||
name: firefly-iii
|
name: firefly-iii
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .domain }}
|
|
||||||
secretName: {{ .tlsSecretName }}
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
version: 6.6.3-1
|
version: 6.6.3-2
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: firefly-iii
|
namespace: firefly-iii
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
domain: firefly-iii.{{ .cloud.domain }}
|
domain: firefly-iii.{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 1Gi
|
storage: 1Gi
|
||||||
timezone: UTC
|
timezone: UTC
|
||||||
adminEmail: '{{ .operator.email }}'
|
adminEmail: '{{ .operator.email }}'
|
||||||
@@ -15,7 +13,7 @@ defaultConfig:
|
|||||||
name: fireflyiii
|
name: fireflyiii
|
||||||
user: fireflyiii
|
user: fireflyiii
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: appKey
|
- key: appKey
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
- postgres.password
|
- postgres.password
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: formbricks
|
name: formbricks
|
||||||
namespace: {{ .namespace }}
|
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:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
@@ -20,7 +16,3 @@ spec:
|
|||||||
name: formbricks
|
name: formbricks
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .domain }}
|
|
||||||
secretName: {{ .tlsSecretName }}
|
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
version: 3.6.0-1
|
version: 3.6.0-2
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
- name: redis
|
- name: redis
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: formbricks
|
namespace: formbricks
|
||||||
externalDnsDomain: "{{ .cloud.domain }}"
|
|
||||||
domain: formbricks.{{ .cloud.domain }}
|
domain: formbricks.{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
db:
|
db:
|
||||||
host: "{{ .apps.postgres.host }}"
|
host: '{{ .apps.postgres.host }}'
|
||||||
port: "{{ .apps.postgres.port }}"
|
port: '{{ .apps.postgres.port }}'
|
||||||
name: formbricks
|
name: formbricks
|
||||||
user: formbricks
|
user: formbricks
|
||||||
redis:
|
redis:
|
||||||
host: "{{ .apps.redis.host }}"
|
host: '{{ .apps.redis.host }}'
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: dbUrl
|
- key: dbUrl
|
||||||
default: "postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable"
|
default: postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host
|
||||||
- key: nextauthSecret
|
}}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable
|
||||||
- key: encryptionKey
|
- key: nextauthSecret
|
||||||
- key: cronSecret
|
- key: encryptionKey
|
||||||
|
- key: cronSecret
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
- postgres.password
|
- postgres.password
|
||||||
- redis.password
|
- redis.password
|
||||||
|
|||||||
21
gancio/notes.md
Normal file
21
gancio/notes.md
Normal 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.
|
||||||
@@ -28,6 +28,6 @@ Key settings in `config.yaml`:
|
|||||||
|
|
||||||
## Notes
|
## 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
|
- 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
|
- Events are federated to Mastodon and other ActivityPub platforms
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: gancio
|
name: gancio
|
||||||
namespace: 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:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
@@ -20,7 +16,3 @@ spec:
|
|||||||
name: gancio
|
name: gancio
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .domain }}
|
|
||||||
secretName: {{ .tlsSecretName }}
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
version: 1.28.2-1
|
version: 1.28.2-2
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: gancio
|
namespace: gancio
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
domain: gancio.{{ .cloud.domain }}
|
domain: gancio.{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
db:
|
db:
|
||||||
host: '{{ .apps.postgres.host }}'
|
host: '{{ .apps.postgres.host }}'
|
||||||
@@ -13,8 +11,9 @@ defaultConfig:
|
|||||||
name: gancio
|
name: gancio
|
||||||
user: gancio
|
user: gancio
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: dbUrl
|
- key: dbUrl
|
||||||
default: 'postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable'
|
default: postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host
|
||||||
|
}}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
- postgres.password
|
- postgres.password
|
||||||
|
|||||||
15
ghost/notes.md
Normal file
15
ghost/notes.md
Normal 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.
|
||||||
@@ -19,6 +19,7 @@ spec:
|
|||||||
mysql -h ${DB_HOSTNAME} -P ${DB_PORT} -u root -p${MYSQL_ROOT_PASSWORD} <<EOF
|
mysql -h ${DB_HOSTNAME} -P ${DB_PORT} -u root -p${MYSQL_ROOT_PASSWORD} <<EOF
|
||||||
CREATE DATABASE IF NOT EXISTS ${DB_DATABASE_NAME};
|
CREATE DATABASE IF NOT EXISTS ${DB_DATABASE_NAME};
|
||||||
CREATE USER IF NOT EXISTS '${DB_USERNAME}'@'%' IDENTIFIED BY '${DB_PASSWORD}';
|
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}'@'%';
|
GRANT ALL PRIVILEGES ON ${DB_DATABASE_NAME}.* TO '${DB_USERNAME}'@'%';
|
||||||
FLUSH PRIVILEGES;
|
FLUSH PRIVILEGES;
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: ghost
|
name: ghost
|
||||||
namespace: 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:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
@@ -20,7 +16,3 @@ spec:
|
|||||||
name: ghost
|
name: ghost
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .domain }}
|
|
||||||
secretName: {{ .tlsSecretName }}
|
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
version: 5.130.6-1
|
version: 5.130.6-2
|
||||||
requires:
|
requires:
|
||||||
- name: mysql
|
- name: mysql
|
||||||
- name: smtp
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: ghost
|
namespace: ghost
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
domain: ghost.{{ .cloud.domain }}
|
domain: ghost.{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
db:
|
db:
|
||||||
host: '{{ .apps.mysql.host }}'
|
host: '{{ .apps.mysql.host }}'
|
||||||
@@ -19,7 +17,7 @@ defaultConfig:
|
|||||||
from: '{{ .apps.smtp.from }}'
|
from: '{{ .apps.smtp.from }}'
|
||||||
user: '{{ .apps.smtp.user }}'
|
user: '{{ .apps.smtp.user }}'
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: smtpPassword
|
- key: smtpPassword
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
- mysql.rootPassword
|
- mysql.rootPassword
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ spec:
|
|||||||
partOf: wild-cloud
|
partOf: wild-cloud
|
||||||
spec:
|
spec:
|
||||||
securityContext:
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1000
|
||||||
fsGroup: 1000
|
fsGroup: 1000
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
terminationGracePeriodSeconds: 60
|
terminationGracePeriodSeconds: 60
|
||||||
containers:
|
containers:
|
||||||
- name: gitea
|
- name: gitea
|
||||||
@@ -78,7 +82,10 @@ spec:
|
|||||||
resources:
|
resources:
|
||||||
{}
|
{}
|
||||||
securityContext:
|
securityContext:
|
||||||
{}
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: temp
|
- name: temp
|
||||||
mountPath: /tmp
|
mountPath: /tmp
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: gitea-public
|
name: gitea-public
|
||||||
namespace: gitea
|
namespace: gitea
|
||||||
annotations:
|
|
||||||
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
|
|
||||||
external-dns.alpha.kubernetes.io/target: "{{ .externalDnsDomain }}"
|
|
||||||
spec:
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
- host: "{{ .domain }}"
|
- host: "{{ .domain }}"
|
||||||
http:
|
http:
|
||||||
@@ -18,7 +16,3 @@ spec:
|
|||||||
name: gitea-http
|
name: gitea-http
|
||||||
port:
|
port:
|
||||||
number: 3000
|
number: 3000
|
||||||
tls:
|
|
||||||
- secretName: "{{ .tlsSecretName }}"
|
|
||||||
hosts:
|
|
||||||
- "{{ .domain }}"
|
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
version: 1.24.3-2
|
version: 1.24.3-5
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
- name: smtp
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: gitea
|
namespace: gitea
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
appName: Gitea
|
appName: Gitea
|
||||||
domain: gitea.{{ .cloud.domain }}
|
domain: gitea.{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
adminUser: admin
|
adminUser: admin
|
||||||
adminEmail: "{{ .operator.email }}"
|
adminEmail: '{{ .operator.email }}'
|
||||||
db:
|
db:
|
||||||
name: gitea
|
name: gitea
|
||||||
user: gitea
|
user: gitea
|
||||||
@@ -22,10 +20,10 @@ defaultConfig:
|
|||||||
user: '{{ .apps.smtp.user }}'
|
user: '{{ .apps.smtp.user }}'
|
||||||
from: '{{ .apps.smtp.from }}'
|
from: '{{ .apps.smtp.from }}'
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: adminPassword
|
- key: adminPassword
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: secretKey
|
- key: secretKey
|
||||||
- key: jwtSecret
|
- key: jwtSecret
|
||||||
- key: smtpPassword
|
- key: smtpPassword
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
- postgres.password
|
- postgres.password
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ name: headlamp
|
|||||||
is: headlamp
|
is: headlamp
|
||||||
description: Modern Kubernetes web UI (SIG UI) with in-cluster authentication
|
description: Modern Kubernetes web UI (SIG UI) with in-cluster authentication
|
||||||
category: services
|
category: services
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/headlamp.svg
|
||||||
latest: "v0"
|
latest: "v0"
|
||||||
|
|||||||
@@ -1,64 +1,18 @@
|
|||||||
---
|
apiVersion: networking.k8s.io/v1
|
||||||
apiVersion: traefik.io/v1alpha1
|
kind: Ingress
|
||||||
kind: Middleware
|
|
||||||
metadata:
|
metadata:
|
||||||
name: internal-only
|
name: headlamp
|
||||||
namespace: headlamp
|
namespace: headlamp
|
||||||
spec:
|
spec:
|
||||||
ipWhiteList:
|
ingressClassName: traefik
|
||||||
sourceRange:
|
rules:
|
||||||
- 127.0.0.1/32
|
- host: headlamp.{{ .internalDomain }}
|
||||||
- 10.0.0.0/8
|
http:
|
||||||
- 172.16.0.0/12
|
paths:
|
||||||
- 192.168.0.0/16
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
---
|
backend:
|
||||||
apiVersion: traefik.io/v1alpha1
|
service:
|
||||||
kind: Middleware
|
name: headlamp
|
||||||
metadata:
|
port:
|
||||||
name: headlamp-redirect-scheme
|
number: 80
|
||||||
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
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
version: v0.42.0
|
version: v0.42.0-1
|
||||||
requires:
|
|
||||||
- name: traefik
|
|
||||||
- name: cert-manager
|
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: headlamp
|
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:
|
deploy:
|
||||||
waitForRollout:
|
waitForRollout:
|
||||||
name: headlamp
|
name: headlamp
|
||||||
timeout: "120s"
|
timeout: 120s
|
||||||
|
|||||||
36
headscale/notes.md
Normal file
36
headscale/notes.md
Normal 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>
|
||||||
|
```
|
||||||
@@ -3,10 +3,6 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: headscale
|
name: headscale
|
||||||
namespace: 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:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
@@ -20,7 +16,3 @@ spec:
|
|||||||
name: headscale
|
name: headscale
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .domain }}
|
|
||||||
secretName: {{ .tlsSecretName }}
|
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
version: 0.29.1-1
|
version: 0.29.1-2
|
||||||
scripts:
|
scripts:
|
||||||
- name: register-node
|
- name: register-node
|
||||||
description: Register a Tailscale node using the auth ID shown in the Tailscale app.
|
description: Register a Tailscale node using the auth ID shown in the Tailscale
|
||||||
path: scripts/register-node.sh
|
app.
|
||||||
params:
|
path: scripts/register-node.sh
|
||||||
- name: AUTH_ID
|
params:
|
||||||
description: The auth ID shown by the Tailscale app (e.g. hskey-authreq-...)
|
- name: AUTH_ID
|
||||||
required: true
|
description: The auth ID shown by the Tailscale app (e.g. hskey-authreq-...)
|
||||||
- name: USER
|
required: true
|
||||||
description: Headscale username to register the node under
|
- name: USER
|
||||||
required: true
|
description: Headscale username to register the node under
|
||||||
|
required: true
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: headscale
|
namespace: headscale
|
||||||
domain: headscale.{{ .cloud.domain }}
|
domain: headscale.{{ .cloud.domain }}
|
||||||
externalDnsDomain: "{{ .cloud.domain }}"
|
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
vpnBaseDomain: vpn.{{ .cloud.domain }}
|
vpnBaseDomain: vpn.{{ .cloud.domain }}
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ spec:
|
|||||||
app: immich-machine-learning
|
app: immich-machine-learning
|
||||||
component: machine-learning
|
component: machine-learning
|
||||||
spec:
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1000
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
containers:
|
containers:
|
||||||
- image: "ghcr.io/immich-app/immich-machine-learning:v1.135.3"
|
- image: "ghcr.io/immich-app/immich-machine-learning:v1.135.3"
|
||||||
name: immich-machine-learning
|
name: immich-machine-learning
|
||||||
@@ -25,6 +30,11 @@ spec:
|
|||||||
env:
|
env:
|
||||||
- name: TZ
|
- name: TZ
|
||||||
value: "UTC"
|
value: "UTC"
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- mountPath: /cache
|
- mountPath: /cache
|
||||||
name: immich-cache
|
name: immich-cache
|
||||||
|
|||||||
@@ -9,16 +9,18 @@ spec:
|
|||||||
matchLabels:
|
matchLabels:
|
||||||
app: immich-microservices
|
app: immich-microservices
|
||||||
strategy:
|
strategy:
|
||||||
rollingUpdate:
|
type: Recreate
|
||||||
maxSurge: 0
|
|
||||||
maxUnavailable: 1
|
|
||||||
type: RollingUpdate
|
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: immich-microservices
|
app: immich-microservices
|
||||||
component: microservices
|
component: microservices
|
||||||
spec:
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1000
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
containers:
|
containers:
|
||||||
- image: "ghcr.io/immich-app/immich-server:v1.135.3"
|
- image: "ghcr.io/immich-app/immich-server:v1.135.3"
|
||||||
name: immich-microservices
|
name: immich-microservices
|
||||||
@@ -43,6 +45,11 @@ spec:
|
|||||||
value: "UTC"
|
value: "UTC"
|
||||||
- name: IMMICH_WORKERS_EXCLUDE
|
- name: IMMICH_WORKERS_EXCLUDE
|
||||||
value: api
|
value: api
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- mountPath: /usr/src/app/upload
|
- mountPath: /usr/src/app/upload
|
||||||
name: immich-storage
|
name: immich-storage
|
||||||
|
|||||||
@@ -9,16 +9,18 @@ spec:
|
|||||||
matchLabels:
|
matchLabels:
|
||||||
app: immich-server
|
app: immich-server
|
||||||
strategy:
|
strategy:
|
||||||
rollingUpdate:
|
type: Recreate
|
||||||
maxSurge: 0
|
|
||||||
maxUnavailable: 1
|
|
||||||
type: RollingUpdate
|
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: immich-server
|
app: immich-server
|
||||||
component: server
|
component: server
|
||||||
spec:
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1000
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
containers:
|
containers:
|
||||||
- image: "ghcr.io/immich-app/immich-server:v1.135.3"
|
- image: "ghcr.io/immich-app/immich-server:v1.135.3"
|
||||||
name: immich-server
|
name: immich-server
|
||||||
@@ -46,6 +48,11 @@ spec:
|
|||||||
value: "UTC"
|
value: "UTC"
|
||||||
- name: IMMICH_WORKERS_EXCLUDE
|
- name: IMMICH_WORKERS_EXCLUDE
|
||||||
value: microservices
|
value: microservices
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- mountPath: /usr/src/app/upload
|
- mountPath: /usr/src/app/upload
|
||||||
name: immich-storage
|
name: immich-storage
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ apiVersion: networking.k8s.io/v1
|
|||||||
kind: Ingress
|
kind: Ingress
|
||||||
metadata:
|
metadata:
|
||||||
name: immich-public
|
name: immich-public
|
||||||
annotations:
|
|
||||||
external-dns.alpha.kubernetes.io/target: "{{ .externalDnsDomain }}"
|
|
||||||
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
|
|
||||||
spec:
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
- host: "{{ .domain }}"
|
- host: "{{ .domain }}"
|
||||||
http:
|
http:
|
||||||
@@ -18,7 +16,3 @@ spec:
|
|||||||
name: immich-server
|
name: immich-server
|
||||||
port:
|
port:
|
||||||
number: 3001
|
number: 3001
|
||||||
tls:
|
|
||||||
- secretName: wildcard-wild-cloud-tls
|
|
||||||
hosts:
|
|
||||||
- "{{ .domain }}"
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
version: 1.135.3-2
|
version: 1.135.3-5
|
||||||
requires:
|
requires:
|
||||||
- name: redis
|
- name: redis
|
||||||
- name: postgres
|
- name: postgres
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: immich
|
namespace: immich
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
|
||||||
storage: 5Gi
|
storage: 5Gi
|
||||||
cacheStorage: 5Gi
|
cacheStorage: 5Gi
|
||||||
domain: immich.{{ .cloud.domain }}
|
domain: immich.{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
db:
|
db:
|
||||||
host: '{{ .apps.postgres.host }}'
|
host: '{{ .apps.postgres.host }}'
|
||||||
name: immich
|
name: immich
|
||||||
@@ -16,7 +14,7 @@ defaultConfig:
|
|||||||
redis:
|
redis:
|
||||||
host: '{{ .apps.redis.host }}'
|
host: '{{ .apps.redis.host }}'
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
- redis.password
|
- redis.password
|
||||||
- postgres.password
|
- postgres.password
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: indico
|
name: indico
|
||||||
namespace: {{ .namespace }}
|
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:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
@@ -20,7 +16,3 @@ spec:
|
|||||||
name: indico
|
name: indico
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .domain }}
|
|
||||||
secretName: {{ .tlsSecretName }}
|
|
||||||
|
|||||||
@@ -1,32 +1,30 @@
|
|||||||
version: 3.3.12-1
|
version: 3.3.12-2
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
- name: redis
|
- name: redis
|
||||||
- name: smtp
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: indico
|
namespace: indico
|
||||||
externalDnsDomain: "{{ .cloud.domain }}"
|
domain: indico.{{ .cloud.domain }}
|
||||||
domain: "indico.{{ .cloud.domain }}"
|
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
timezone: UTC
|
timezone: UTC
|
||||||
db:
|
db:
|
||||||
host: "{{ .apps.postgres.host }}"
|
host: '{{ .apps.postgres.host }}'
|
||||||
port: "{{ .apps.postgres.port }}"
|
port: '{{ .apps.postgres.port }}'
|
||||||
name: indico
|
name: indico
|
||||||
user: indico
|
user: indico
|
||||||
redis:
|
redis:
|
||||||
host: "{{ .apps.redis.host }}"
|
host: '{{ .apps.redis.host }}'
|
||||||
port: "6379"
|
port: '6379'
|
||||||
smtp:
|
smtp:
|
||||||
host: "{{ .apps.smtp.host }}"
|
host: '{{ .apps.smtp.host }}'
|
||||||
port: "{{ .apps.smtp.port }}"
|
port: '{{ .apps.smtp.port }}'
|
||||||
from: "{{ .apps.smtp.from }}"
|
from: '{{ .apps.smtp.from }}'
|
||||||
user: "{{ .apps.smtp.user }}"
|
user: '{{ .apps.smtp.user }}'
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: secretKey
|
- key: secretKey
|
||||||
requiredSecrets:
|
requiredSecrets:
|
||||||
- postgres.password
|
- postgres.password
|
||||||
- redis.password
|
- redis.password
|
||||||
- smtp.password
|
- smtp.password
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user